├── .gitignore ├── test ├── requirements.txt ├── geom_test.go └── generate-test-data.py ├── .travis.yml ├── carto ├── README.md ├── geojson.go └── map_test.go ├── encoding ├── shp │ ├── testdata │ │ ├── polygons.dbf │ │ ├── polygons.shp │ │ ├── polygons.shx │ │ ├── triangles.shp │ │ ├── triangles.shx │ │ ├── triangles.dbf │ │ ├── polygons.prj │ │ └── triangles.prj │ ├── shp_test.go │ └── polygon_output_test.go ├── osm │ ├── testdata │ │ ├── README.txt │ │ └── honolulu_hawaii.osm.pbf │ ├── check_test.go │ ├── keep_test.go │ ├── check.go │ ├── tags_test.go │ ├── keep.go │ └── tags.go ├── wkt │ ├── wkt.go │ ├── polygon.go │ ├── linestring.go │ ├── multipolygon.go │ ├── multilinestring.go │ ├── wkt_test.go │ ├── encode.go │ └── point.go ├── geojson │ ├── geojson.go │ ├── encode.go │ ├── geojson_test.go │ └── decode.go ├── wkb │ ├── linestring.go │ ├── polygon.go │ ├── multipoint.go │ ├── multipolygon.go │ ├── geometrycollection.go │ ├── multilinestring.go │ ├── point.go │ ├── wkb_test.go │ └── wkb.go └── hex │ ├── hex.go │ └── hex_test.go ├── proj ├── README.md ├── units.go ├── defs.go ├── longlat.go ├── utm.go ├── PrimeMeridian.go ├── global.go ├── adjust_axis.go ├── parseCode.go ├── deriveConstants.go ├── merc.go ├── DatumDef.go ├── eqdc.go ├── common.go ├── aea.go ├── krovak.go ├── datum_transform.go ├── transform.go ├── tmerc.go ├── projString.go ├── lcc.go ├── Proj.go └── EllipsoidDef.go ├── point_test.go ├── go.mod ├── length_test.go ├── clip_test.go ├── geometrycollection.go ├── multipoint.go ├── pointsiter_test.go ├── geom_test.go ├── LICENSE ├── index └── rtree │ ├── LICENSE │ ├── README.md │ └── geom.go ├── route └── route_test.go ├── point.go ├── geom.go ├── linestring.go ├── multilinestring.go ├── within.go ├── within_test.go ├── polygon.go ├── transform.go ├── multipolygon.go ├── simplify_test.go ├── area_test.go ├── area.go ├── intersection.go ├── polygonop_test.go ├── bounds_test.go ├── bounds.go ├── simplify.go ├── op └── geom.go └── similar.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | shapely 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | notifications: 3 | email: false 4 | -------------------------------------------------------------------------------- /carto/README.md: -------------------------------------------------------------------------------- 1 | carto 2 | ===== 3 | 4 | Go language GIS map drawing library 5 | -------------------------------------------------------------------------------- /encoding/shp/testdata/polygons.dbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctessum/geom/HEAD/encoding/shp/testdata/polygons.dbf -------------------------------------------------------------------------------- /encoding/shp/testdata/polygons.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctessum/geom/HEAD/encoding/shp/testdata/polygons.shp -------------------------------------------------------------------------------- /encoding/shp/testdata/polygons.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctessum/geom/HEAD/encoding/shp/testdata/polygons.shx -------------------------------------------------------------------------------- /encoding/shp/testdata/triangles.shp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctessum/geom/HEAD/encoding/shp/testdata/triangles.shp -------------------------------------------------------------------------------- /encoding/shp/testdata/triangles.shx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctessum/geom/HEAD/encoding/shp/testdata/triangles.shx -------------------------------------------------------------------------------- /encoding/osm/testdata/README.txt: -------------------------------------------------------------------------------- 1 | Data from https://mapzen.com/data/metro-extracts/metro/honolulu_hawaii/, downloaded 11/17/2016. 2 | -------------------------------------------------------------------------------- /encoding/osm/testdata/honolulu_hawaii.osm.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ctessum/geom/HEAD/encoding/osm/testdata/honolulu_hawaii.osm.pbf -------------------------------------------------------------------------------- /proj/README.md: -------------------------------------------------------------------------------- 1 | projgeom 2 | ======== 3 | 4 | Go library for geodesic reprojections of Open GIS Consortium-style geometry objects. 5 | -------------------------------------------------------------------------------- /encoding/shp/testdata/triangles.dbf: -------------------------------------------------------------------------------- 1 | _a idN 2 | valueN 26. 11. -------------------------------------------------------------------------------- /encoding/shp/testdata/polygons.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /encoding/shp/testdata/triangles.prj: -------------------------------------------------------------------------------- 1 | GEOGCS["GCS_WGS_1984",DATUM["D_WGS_1984",SPHEROID["WGS_1984",6378137,298.257223563]],PRIMEM["Greenwich",0],UNIT["Degree",0.017453292519943295]] -------------------------------------------------------------------------------- /proj/units.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | type unit struct { 4 | to_meter float64 5 | } 6 | 7 | var units = map[string]unit{ 8 | "ft": unit{to_meter: 0.3048}, 9 | "us-ft": unit{to_meter: 1200. / 3937.}, 10 | } 11 | -------------------------------------------------------------------------------- /encoding/wkt/wkt.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type UnsupportedGeometryError struct { 8 | Type reflect.Type 9 | } 10 | 11 | func (e UnsupportedGeometryError) Error() string { 12 | return "wkt: unsupported geometry type: " + e.Type.String() 13 | } 14 | -------------------------------------------------------------------------------- /proj/defs.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | var defs map[string]*SR 4 | 5 | func addDef(name, def string) error { 6 | if defs == nil { 7 | defs = make(map[string]*SR) 8 | } 9 | proj, err := Parse(def) 10 | if err != nil { 11 | return err 12 | } 13 | defs[name] = proj 14 | return nil 15 | } 16 | -------------------------------------------------------------------------------- /encoding/wkt/polygon.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | ) 6 | 7 | func appendPolygonWKT(dst []byte, polygon geom.Polygon) []byte { 8 | dst = append(dst, []byte("POLYGON(")...) 9 | dst = appendPointssCoords(dst, polygon) 10 | dst = append(dst, ')') 11 | return dst 12 | } 13 | -------------------------------------------------------------------------------- /encoding/wkt/linestring.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | ) 6 | 7 | func appendLineStringWKT(dst []byte, lineString geom.LineString) []byte { 8 | dst = append(dst, []byte("LINESTRING(")...) 9 | dst = appendPointsCoords(dst, lineString) 10 | dst = append(dst, ')') 11 | return dst 12 | } 13 | -------------------------------------------------------------------------------- /encoding/osm/check_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestCheck(t *testing.T) { 9 | f, err := os.Open("testdata/honolulu_hawaii.osm.pbf") 10 | defer f.Close() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | data, err := ExtractTag(f, "source", true, "Bing") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | if err = data.Check(); err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /encoding/osm/keep_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/paulmach/osm" 7 | ) 8 | 9 | func TestKeepTags(t *testing.T) { 10 | k := KeepTags(map[string][]string{"x": {}}) 11 | r := &osm.Node{ 12 | Tags: []osm.Tag{ 13 | { 14 | Key: "x", 15 | Value: "y", 16 | }, 17 | }, 18 | } 19 | keep := k(nil, r) 20 | if !keep { 21 | t.Errorf("keep should be true but is false") 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /point_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestPoint_Buffer(t *testing.T) { 9 | p := Point{X: 1, Y: 1} 10 | buf := p.Buffer(1, 4) 11 | want := Polygon{[]Point{ 12 | Point{X: 2, Y: 1}, 13 | Point{X: 1, Y: 2}, 14 | Point{X: 0, Y: 1.0000000000000002}, 15 | Point{X: 0.9999999999999998, Y: 0}, 16 | }} 17 | if !reflect.DeepEqual(buf, want) { 18 | t.Errorf("have: %#v, want %#v", buf, want) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /proj/longlat.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | // LongLat is a longitude-latitude (i.e., no projection) projection. 4 | func LongLat(this *SR) (forward, inverse Transformer, err error) { 5 | identity := func(x, y float64) (float64, float64, error) { 6 | return x, y, nil 7 | } 8 | forward = identity 9 | inverse = identity 10 | return 11 | } 12 | 13 | func init() { 14 | // Register this projection with the corresponding names. 15 | registerTrans(LongLat, "longlat", "identity") 16 | } 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ctessum/geom 2 | 3 | require ( 4 | github.com/ctessum/polyclip-go v1.1.0 5 | github.com/jonas-p/go-shp v0.1.2-0.20190401125246-9fd306ae10a6 6 | github.com/llgcode/draw2d v0.0.0-20180817132918-587a55234ca2 // indirect 7 | github.com/paulmach/osm v0.1.1 8 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 9 | gonum.org/v1/gonum v0.9.3 10 | gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e // indirect 11 | gonum.org/v1/plot v0.9.0 12 | ) 13 | 14 | go 1.13 15 | -------------------------------------------------------------------------------- /encoding/geojson/geojson.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | type Geometry struct { 4 | Type string `json:"type"` 5 | Coordinates interface{} `json:"coordinates"` 6 | } 7 | 8 | type InvalidGeometryError struct{} 9 | 10 | func (e InvalidGeometryError) Error() string { 11 | return "geojson: invalid geometry" 12 | } 13 | 14 | type UnsupportedGeometryError struct { 15 | Type string 16 | } 17 | 18 | func (e UnsupportedGeometryError) Error() string { 19 | return "geojson: unsupported geometry type " + e.Type 20 | } 21 | -------------------------------------------------------------------------------- /encoding/wkb/linestring.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func lineStringReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | points, err := readPoints(r, byteOrder) 11 | if err != nil { 12 | return nil, err 13 | } 14 | return geom.LineString(points), nil 15 | } 16 | 17 | func writeLineString(w io.Writer, byteOrder binary.ByteOrder, lineString geom.LineString) error { 18 | return writePoints(w, byteOrder, lineString) 19 | } 20 | -------------------------------------------------------------------------------- /encoding/wkt/multipolygon.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | ) 6 | 7 | func appendMultiPolygonWKT(dst []byte, 8 | multiPolygon geom.MultiPolygon) []byte { 9 | dst = append(dst, []byte("MULTIPOLYGON((")...) 10 | for i, pg := range multiPolygon { 11 | dst = appendPointssCoords(dst, pg) 12 | if i != len(multiPolygon)-1 { 13 | dst = append(dst, ')') 14 | dst = append(dst, ',') 15 | dst = append(dst, '(') 16 | } 17 | } 18 | dst = append(dst, ')') 19 | dst = append(dst, ')') 20 | return dst 21 | } 22 | -------------------------------------------------------------------------------- /encoding/wkt/multilinestring.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | ) 6 | 7 | func appendMultiLineStringWKT(dst []byte, 8 | multiLineString geom.MultiLineString) []byte { 9 | dst = append(dst, []byte("MULTILINESTRING((")...) 10 | for i, ls := range multiLineString { 11 | dst = appendPointsCoords(dst, ls) 12 | if i != len(multiLineString)-1 { 13 | dst = append(dst, ')') 14 | dst = append(dst, ',') 15 | dst = append(dst, '(') 16 | } 17 | } 18 | dst = append(dst, ')') 19 | dst = append(dst, ')') 20 | return dst 21 | } 22 | -------------------------------------------------------------------------------- /encoding/hex/hex.go: -------------------------------------------------------------------------------- 1 | package hex 2 | 3 | import ( 4 | "encoding/binary" 5 | "encoding/hex" 6 | "github.com/ctessum/geom" 7 | "github.com/ctessum/geom/encoding/wkb" 8 | ) 9 | 10 | func Encode(g geom.Geom, byteOrder binary.ByteOrder) (string, error) { 11 | wkb, err := wkb.Encode(g, byteOrder) 12 | if err != nil { 13 | return "", err 14 | } 15 | return hex.EncodeToString(wkb), nil 16 | } 17 | 18 | func Decode(s string) (geom.Geom, error) { 19 | data, err := hex.DecodeString(s) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return wkb.Decode(data) 24 | } 25 | -------------------------------------------------------------------------------- /encoding/hex/hex_test.go: -------------------------------------------------------------------------------- 1 | package hex 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | "github.com/ctessum/geom/encoding/wkb" 6 | "testing" 7 | ) 8 | 9 | func Test(t *testing.T) { 10 | var cases = []struct { 11 | g geom.Geom 12 | ndr string 13 | }{ 14 | { 15 | geom.Point{1, 2}, 16 | "0101000000000000000000f03f0000000000000040", 17 | }, 18 | } 19 | for _, c := range cases { 20 | if got, err := Encode(c.g, wkb.NDR); err != nil || got != c.ndr { 21 | t.Errorf("Encode(%#v, %#v) == %#v, %#v, want %#v, nil", c.g, wkb.NDR, got, err, c.ndr) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /proj/utm.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // UTM is a universal transverse Mercator projection. 9 | func UTM(this *SR) (forward, inverse Transformer, err error) { 10 | 11 | if math.IsNaN(this.Zone) { 12 | err = fmt.Errorf("in proj.UTM: zone is not specified") 13 | return 14 | } 15 | this.Lat0 = 0 16 | this.Long0 = ((6 * math.Abs(this.Zone)) - 183) * deg2rad 17 | this.X0 = 500000 18 | if this.UTMSouth { 19 | this.Y0 = 10000000 20 | } else { 21 | this.Y0 = 0 22 | } 23 | this.K0 = 0.9996 24 | 25 | return TMerc(this) 26 | } 27 | 28 | func init() { 29 | registerTrans(UTM, "Universal Transverse Mercator System", "utm") 30 | } 31 | -------------------------------------------------------------------------------- /encoding/wkb/polygon.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func polygonReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | var numRings uint32 11 | if err := binary.Read(r, byteOrder, &numRings); err != nil { 12 | return nil, err 13 | } 14 | rings := make([]geom.Path, numRings) 15 | for i := uint32(0); i < numRings; i++ { 16 | if points, err := readPoints(r, byteOrder); err != nil { 17 | return nil, err 18 | } else { 19 | rings[i] = points 20 | } 21 | } 22 | return geom.Polygon(rings), nil 23 | } 24 | 25 | func writePolygon(w io.Writer, byteOrder binary.ByteOrder, polygon geom.Polygon) error { 26 | return writePointss(w, byteOrder, polygon) 27 | } 28 | -------------------------------------------------------------------------------- /length_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "testing" 4 | 5 | func TestLength(t *testing.T) { 6 | tests := []struct { 7 | test Linear 8 | expected float64 9 | }{ 10 | { 11 | test: MultiLineString{ 12 | { 13 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 14 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 15 | Point{X: 0, Y: 0}, 16 | }, 17 | { 18 | Point{X: 0.5, Y: 0.5}, Point{X: 0.5, Y: 1.5}, 19 | Point{X: 1.5, Y: 1.5}, Point{X: 1.5, Y: 0.5}, 20 | Point{X: 0.5, Y: 0.5}, 21 | }, 22 | }, 23 | expected: 12., 24 | }, 25 | } 26 | for i, test := range tests { 27 | result := test.test.Length() 28 | if result != test.expected { 29 | t.Errorf("%d: expected %g, got %g", i, test.expected, result) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /encoding/wkt/wkt_test.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestWKT(t *testing.T) { 10 | var testCases = []struct { 11 | g geom.Geom 12 | wkt []byte 13 | }{ 14 | { 15 | geom.Point{1, 2}, 16 | []byte(`POINT(1 2)`), 17 | }, 18 | { 19 | geom.LineString([]geom.Point{{1, 2}, {3, 4}}), 20 | []byte(`LINESTRING(1 2,3 4)`), 21 | }, 22 | { 23 | geom.Polygon([]geom.Path{{{1, 2}, {3, 4}, {5, 6}, {1, 2}}}), 24 | []byte(`POLYGON((1 2,3 4,5 6,1 2))`), 25 | }, 26 | } 27 | for _, tc := range testCases { 28 | if got, err := Encode(tc.g); err != nil || !reflect.DeepEqual(got, tc.wkt) { 29 | t.Errorf("Encode(%#v) == %#v, %#v, want %#v, nil", tc.g, got, err, tc.wkt) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /proj/PrimeMeridian.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | var primeMeridian = map[string]float64{ 4 | "greenwich": 0.0, //"0dE", 5 | "lisbon": -9.131906111111, //"9d07'54.862\"W", 6 | "paris": 2.337229166667, //"2d20'14.025\"E", 7 | "bogota": -74.080916666667, //"74d04'51.3\"W", 8 | "madrid": -3.687938888889, //"3d41'16.58\"W", 9 | "rome": 12.452333333333, //"12d27'8.4\"E", 10 | "bern": 7.439583333333, //"7d26'22.5\"E", 11 | "jakarta": 106.807719444444, //"106d48'27.79\"E", 12 | "ferro": -17.666666666667, //"17d40'W", 13 | "brussels": 4.367975, //"4d22'4.71\"E", 14 | "stockholm": 18.058277777778, //"18d3'29.8\"E", 15 | "athens": 23.7163375, //"23d42'58.815\"E", 16 | "oslo": 10.722916666667, //"10d43'22.5\"E" 17 | } 18 | -------------------------------------------------------------------------------- /encoding/wkt/encode.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | "reflect" 6 | ) 7 | 8 | func Encode(g geom.Geom) ([]byte, error) { 9 | switch g.(type) { 10 | case geom.Point: 11 | point := g.(geom.Point) 12 | return appendPointWKT(nil, &point), nil 13 | case geom.LineString: 14 | lineString := g.(geom.LineString) 15 | return appendLineStringWKT(nil, lineString), nil 16 | case geom.MultiLineString: 17 | multiLineString := g.(geom.MultiLineString) 18 | return appendMultiLineStringWKT(nil, multiLineString), nil 19 | case geom.Polygon: 20 | polygon := g.(geom.Polygon) 21 | return appendPolygonWKT(nil, polygon), nil 22 | case geom.MultiPolygon: 23 | multiPolygon := g.(geom.MultiPolygon) 24 | return appendMultiPolygonWKT(nil, multiPolygon), nil 25 | default: 26 | return nil, &UnsupportedGeometryError{reflect.TypeOf(g)} 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /proj/global.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | func init() { 4 | err := addDef("EPSG:4326", "+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +units=degrees") 5 | if err != nil { 6 | panic(err) 7 | } 8 | err = addDef("EPSG:4269", "+title=NAD83 (long/lat) +proj=longlat +a=6378137.0 +b=6356752.31414036 +ellps=GRS80 +datum=NAD83 +units=degrees") 9 | if err != nil { 10 | panic(err) 11 | } 12 | err = addDef("EPSG:3857", "+title=WGS 84 / Pseudo-Mercator +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs") 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | defs["WGS84"] = defs["EPSG:4326"] 18 | defs["EPSG:3785"] = defs["EPSG:3857"] // maintain backward compat, official code is 3857 19 | defs["GOOGLE"] = defs["EPSG:3857"] 20 | defs["EPSG:900913"] = defs["EPSG:3857"] 21 | defs["EPSG:102113"] = defs["EPSG:3857"] 22 | } 23 | -------------------------------------------------------------------------------- /clip_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestClip(t *testing.T) { 9 | subject := LineString{ 10 | {0, 1}, {1.25, 1}, {1.5, 1.1}, {1.75, 1}, {5, 1}, 11 | {5, 2}, {0, 2}, 12 | } 13 | clipping := Polygon{ 14 | {{1, 0}, {4, 0}, {4, 3}, {1, 3}}, 15 | {{2, 0.5}, {3, 0.5}, {3, 2.5}, {2, 2.5}}, 16 | } 17 | want := MultiLineString{ 18 | {Point{X: 2, Y: 1}, Point{X: 1.75, Y: 1}, Point{X: 1.5, Y: 1.1}, Point{X: 1.25, Y: 1}, Point{X: 1, Y: 1}}, 19 | {Point{X: 2, Y: 2}, Point{X: 1, Y: 2}}, 20 | {Point{X: 4, Y: 1}, Point{X: 3, Y: 1}}, 21 | {Point{X: 4, Y: 2}, Point{X: 3, Y: 2}}, 22 | } 23 | r := subject.Clip(clipping) 24 | if !reflect.DeepEqual(r, want) { 25 | t.Errorf("LineString: %+v \n\t!= %+v", r, want) 26 | } 27 | r2 := MultiLineString{subject}.Clip(clipping) 28 | if !reflect.DeepEqual(r2, want) { 29 | t.Errorf("MultiLineString: %+v \n\t!= %+v", r2, want) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /geometrycollection.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | // GeometryCollection is a holder for multiple related geometry objects of 4 | // arbitrary type. 5 | type GeometryCollection []Geom 6 | 7 | // Bounds gives the rectangular extents of the GeometryCollection. 8 | func (gc GeometryCollection) Bounds() *Bounds { 9 | b := NewBounds() 10 | for _, geom := range gc { 11 | b.Extend(geom.Bounds()) 12 | } 13 | return b 14 | } 15 | 16 | // Len returns the number of points in the receiver. 17 | func (gc GeometryCollection) Len() int { 18 | var i int 19 | for _, g := range gc { 20 | i += g.Len() 21 | } 22 | return i 23 | } 24 | 25 | // Points returns an iterator for the points in the receiver. 26 | func (gc GeometryCollection) Points() func() Point { 27 | var i, j int 28 | p := gc[0].Points() 29 | return func() Point { 30 | if i == gc[j].Len() { 31 | j++ 32 | i = 0 33 | p = gc[j].Points() 34 | } 35 | i++ 36 | return p() 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /multipoint.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | // MultiPoint is a holder for multiple related points. 4 | type MultiPoint []Point 5 | 6 | // Bounds gives the rectangular extents of the MultiPoint. 7 | func (mp MultiPoint) Bounds() *Bounds { 8 | b := NewBounds() 9 | for _, p := range mp { 10 | b.extendPoint(p) 11 | } 12 | return b 13 | } 14 | 15 | // Within calculates whether all of the points in mp are within poly or touching 16 | // its edge. 17 | func (mp MultiPoint) Within(poly Polygonal) WithinStatus { 18 | for _, p := range mp { 19 | if pointInPolygonal(p, poly) == Outside { 20 | return Outside 21 | } 22 | } 23 | return Inside 24 | } 25 | 26 | // Len returns the number of points in the receiver. 27 | func (mp MultiPoint) Len() int { return len(mp) } 28 | 29 | // Points returns an iterator for the points in the receiver. 30 | func (mp MultiPoint) Points() func() Point { 31 | var i int 32 | return func() Point { 33 | i++ 34 | return mp[i-1] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pointsiter_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGeom_points(t *testing.T) { 8 | tests := []Geom{ 9 | Point{0, 1}, 10 | MultiPoint{{0, 1}, {0, 2}}, 11 | LineString{{0, 1}, {0, 2}}, 12 | MultiLineString{{{0, 1}, {0, 2}}, {{0, 3}, {0, 4}}}, 13 | Polygon{ 14 | {{0, 1}, {0, 2}}, {{0, 3}, {0, 4}}, 15 | {{0, 5}, {0, 6}}, {{0, 7}, {0, 8}}, 16 | }, 17 | MultiPolygon{ 18 | { 19 | {{0, 1}, {0, 2}}, {{0, 3}, {0, 4}}, 20 | {{0, 5}, {0, 6}}, {{0, 7}, {0, 8}}, 21 | }, 22 | { 23 | {{0, 9}, {0, 10}}, 24 | {{0, 11}, {0, 12}}, 25 | }, 26 | }, 27 | GeometryCollection{ 28 | Point{0, 1}, 29 | MultiPoint{{0, 2}, {0, 3}}, 30 | }, 31 | } 32 | 33 | for j, test := range tests { 34 | pf := test.Points() 35 | for i := 0; i < test.Len(); i++ { 36 | want := Point{0, float64(i + 1)} 37 | p := pf() 38 | if p != want { 39 | t.Errorf("test %d index %d: %+v != %+v", j, i, p, want) 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /proj/adjust_axis.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import "fmt" 4 | 5 | func adjust_axis(crs *SR, denorm bool, point []float64) ([]float64, error) { 6 | var v float64 7 | var t int 8 | for i := 0; i < 3; i++ { 9 | if denorm && i == 2 && len(point) == 2 { 10 | continue 11 | } 12 | if i == 0 { 13 | v = point[0] 14 | t = 0 15 | } else if i == 1 { 16 | v = point[1] 17 | t = 1 18 | } else { 19 | v = point[2] 20 | t = 2 21 | } 22 | switch crs.Axis[i] { 23 | case 'e': 24 | point[t] = v 25 | break 26 | case 'w': 27 | point[t] = -v 28 | break 29 | case 'n': 30 | point[t] = v 31 | break 32 | case 's': 33 | point[t] = -v 34 | break 35 | case 'u': 36 | if len(point) == 3 { 37 | point[2] = v 38 | } 39 | break 40 | case 'd': 41 | if len(point) == 3 { 42 | point[2] = -v 43 | } 44 | break 45 | default: 46 | err := fmt.Errorf("in plot.adjust_axis: unknown axis (%v). check "+ 47 | "definition of %s", crs.Axis[i], crs.Name) 48 | return nil, err 49 | } 50 | } 51 | return point, nil 52 | } 53 | -------------------------------------------------------------------------------- /encoding/wkb/multipoint.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func multiPointReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | var numPoints uint32 11 | if err := binary.Read(r, byteOrder, &numPoints); err != nil { 12 | return nil, err 13 | } 14 | points := make([]geom.Point, numPoints) 15 | for i := uint32(0); i < numPoints; i++ { 16 | if g, err := Read(r); err == nil { 17 | var ok bool 18 | points[i], ok = g.(geom.Point) 19 | if !ok { 20 | return nil, &UnexpectedGeometryError{g} 21 | } 22 | } else { 23 | return nil, err 24 | } 25 | } 26 | return geom.MultiPoint(points), nil 27 | } 28 | 29 | func writeMultiPoint(w io.Writer, byteOrder binary.ByteOrder, multiPoint geom.MultiPoint) error { 30 | if err := binary.Write(w, byteOrder, uint32(len(multiPoint))); err != nil { 31 | return err 32 | } 33 | for _, point := range multiPoint { 34 | if err := Write(w, byteOrder, point); err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /encoding/wkt/point.go: -------------------------------------------------------------------------------- 1 | package wkt 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | "strconv" 6 | ) 7 | 8 | func appendPointCoords(dst []byte, point *geom.Point) []byte { 9 | dst = strconv.AppendFloat(dst, point.X, 'g', -1, 64) 10 | dst = append(dst, ' ') 11 | dst = strconv.AppendFloat(dst, point.Y, 'g', -1, 64) 12 | return dst 13 | } 14 | 15 | func appendPointsCoords(dst []byte, points []geom.Point) []byte { 16 | for i, point := range points { 17 | if i != 0 { 18 | dst = append(dst, ',') 19 | } 20 | dst = appendPointCoords(dst, &point) 21 | } 22 | return dst 23 | } 24 | 25 | func appendPointssCoords(dst []byte, pointss []geom.Path) []byte { 26 | for i, points := range pointss { 27 | if i != 0 { 28 | dst = append(dst, ',') 29 | } 30 | dst = append(dst, '(') 31 | dst = appendPointsCoords(dst, points) 32 | dst = append(dst, ')') 33 | } 34 | return dst 35 | } 36 | 37 | func appendPointWKT(dst []byte, point *geom.Point) []byte { 38 | dst = append(dst, []byte("POINT(")...) 39 | dst = appendPointCoords(dst, point) 40 | dst = append(dst, ')') 41 | return dst 42 | } 43 | -------------------------------------------------------------------------------- /encoding/wkb/multipolygon.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func multiPolygonReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | var numPolygons uint32 11 | if err := binary.Read(r, byteOrder, &numPolygons); err != nil { 12 | return nil, err 13 | } 14 | polygons := make([]geom.Polygon, numPolygons) 15 | for i := uint32(0); i < numPolygons; i++ { 16 | if g, err := Read(r); err == nil { 17 | var ok bool 18 | polygons[i], ok = g.(geom.Polygon) 19 | if !ok { 20 | return nil, &UnexpectedGeometryError{g} 21 | } 22 | } else { 23 | return nil, err 24 | } 25 | } 26 | return geom.MultiPolygon(polygons), nil 27 | } 28 | 29 | func writeMultiPolygon(w io.Writer, byteOrder binary.ByteOrder, multiPolygon geom.MultiPolygon) error { 30 | if err := binary.Write(w, byteOrder, uint32(len(multiPolygon))); err != nil { 31 | return err 32 | } 33 | for _, polygon := range multiPolygon { 34 | if err := Write(w, byteOrder, polygon); err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /proj/parseCode.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | func testWKT(code string) bool { 9 | var codeWords = []string{"GEOGCS", "GEOCCS", "PROJCS", "LOCAL_CS"} 10 | for _, c := range codeWords { 11 | if strings.Contains(code, c) { 12 | return true 13 | } 14 | } 15 | return false 16 | } 17 | 18 | func testProj(code string) bool { 19 | return len(code) >= 1 && code[0] == '+' 20 | } 21 | 22 | // Parse parses a WKT- or PROJ4-formatted projection string into a Proj object. 23 | func Parse(code string) (*SR, error) { 24 | //check to see if this is a WKT string 25 | if p, ok := defs[code]; ok { 26 | return p, nil 27 | } else if testWKT(code) { 28 | p, err := wkt(code) 29 | if err != nil { 30 | return nil, err 31 | } 32 | p.DeriveConstants() 33 | return p, nil 34 | } else if testProj(code) { 35 | p, err := projString(code) 36 | if err != nil { 37 | return nil, err 38 | } 39 | p.DeriveConstants() 40 | return p, nil 41 | } 42 | return nil, fmt.Errorf("unsupported projection definition '%s'; only proj4 and "+ 43 | "WKT are supported", code) 44 | } 45 | -------------------------------------------------------------------------------- /geom_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestBounds(t *testing.T) { 9 | 10 | var testCases = []struct { 11 | g Geom 12 | bounds *Bounds 13 | }{ 14 | { 15 | Point{1, 2}, 16 | &Bounds{Point{1, 2}, Point{1, 2}}, 17 | }, 18 | { 19 | LineString([]Point{{1, 2}, {3, 4}}), 20 | &Bounds{Point{1, 2}, Point{3, 4}}, 21 | }, 22 | { 23 | Polygon([]Path{{{1, 2}, {3, 4}, {5, 6}}}), 24 | &Bounds{Point{1, 2}, Point{5, 6}}, 25 | }, 26 | { 27 | MultiPoint([]Point{{1, 2}, {3, 4}}), 28 | &Bounds{Point{1, 2}, Point{3, 4}}, 29 | }, 30 | { 31 | MultiLineString([]LineString{[]Point{{1, 2}, {3, 4}}, []Point{{5, 6}, {7, 8}}}), 32 | &Bounds{Point{1, 2}, Point{7, 8}}, 33 | }, 34 | } 35 | 36 | for _, tc := range testCases { 37 | if got := tc.g.Bounds(); !reflect.DeepEqual(got, tc.bounds) { 38 | t.Errorf("%#v.Bounds() == %#v, want %#v", tc.g, got, tc.bounds) 39 | } 40 | } 41 | 42 | } 43 | 44 | func TestBoundsEmpty(t *testing.T) { 45 | if got := NewBounds().Empty(); got != true { 46 | t.Errorf("NewBounds.Empty() == %#v, want true", got) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /encoding/wkb/geometrycollection.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func geometryCollectionReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | var numGeometries uint32 11 | if err := binary.Read(r, byteOrder, &numGeometries); err != nil { 12 | return nil, err 13 | } 14 | geoms := make([]geom.Geom, numGeometries) 15 | for i := uint32(0); i < numGeometries; i++ { 16 | if g, err := Read(r); err == nil { 17 | var ok bool 18 | geoms[i], ok = g.(geom.Geom) 19 | if !ok { 20 | return nil, &UnexpectedGeometryError{g} 21 | } 22 | } else { 23 | return nil, err 24 | } 25 | } 26 | return geom.GeometryCollection(geoms), nil 27 | } 28 | 29 | func writeGeometryCollection(w io.Writer, byteOrder binary.ByteOrder, geometryCollection geom.GeometryCollection) error { 30 | if err := binary.Write(w, byteOrder, uint32(len(geometryCollection))); err != nil { 31 | return err 32 | } 33 | for _, geom := range geometryCollection { 34 | if err := Write(w, byteOrder, geom); err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /encoding/wkb/multilinestring.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func multiLineStringReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | var numLineStrings uint32 11 | if err := binary.Read(r, byteOrder, &numLineStrings); err != nil { 12 | return nil, err 13 | } 14 | lineStrings := make([]geom.LineString, numLineStrings) 15 | for i := uint32(0); i < numLineStrings; i++ { 16 | if g, err := Read(r); err == nil { 17 | var ok bool 18 | lineStrings[i], ok = g.(geom.LineString) 19 | if !ok { 20 | return nil, &UnexpectedGeometryError{g} 21 | } 22 | } else { 23 | return nil, err 24 | } 25 | } 26 | return geom.MultiLineString(lineStrings), nil 27 | } 28 | 29 | func writeMultiLineString(w io.Writer, byteOrder binary.ByteOrder, multiLineString geom.MultiLineString) error { 30 | if err := binary.Write(w, byteOrder, uint32(len(multiLineString))); err != nil { 31 | return err 32 | } 33 | for _, lineString := range multiLineString { 34 | if err := Write(w, byteOrder, lineString); err != nil { 35 | return err 36 | } 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /test/geom_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "github.com/ctessum/geom/encoding/hex" 5 | "github.com/ctessum/geom/encoding/wkb" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestHexEncode(t *testing.T) { 11 | for _, c := range cases { 12 | if got, err := hex.Encode(c.g, wkb.NDR); err != nil || got != c.hex { 13 | t.Errorf("hex.Encode(%#v, %#v) == %#v, %#v, want %#v, nil", c.g, wkb.NDR, got, err, c.hex) 14 | } 15 | } 16 | } 17 | 18 | func TestHexDecode(t *testing.T) { 19 | for _, c := range cases { 20 | if got, err := hex.Decode(c.hex); err != nil || !reflect.DeepEqual(got, c.g) { 21 | t.Errorf("hex.Decode(%#v) == %#v, %#v, want %#v, nil", c.wkb, got, err, c.g) 22 | } 23 | } 24 | } 25 | 26 | func TestWKBDecode(t *testing.T) { 27 | for _, c := range cases { 28 | if got, err := wkb.Encode(c.g, wkb.NDR); err != nil || !reflect.DeepEqual(got, c.wkb) { 29 | t.Errorf("wkb.Encode(%#v, %#v) == %#v, %#v, want %#v, nil", c.g, wkb.NDR, got, err, c.wkb) 30 | } 31 | } 32 | } 33 | 34 | func TestWKBEncode(t *testing.T) { 35 | for _, c := range cases { 36 | if got, err := wkb.Decode(c.wkb); err != nil || !reflect.DeepEqual(got, c.g) { 37 | t.Errorf("wkb.Decode(%#v) == %#v, %#v, want %#v, nil", c.wkb, got, err, c.g) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2916, Tom Payne and Chris Tessum. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /index/rtree/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Daniel Connelly. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | Neither the name of Daniel Connelly nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 10 | 11 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /encoding/wkb/point.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "encoding/binary" 5 | "github.com/ctessum/geom" 6 | "io" 7 | ) 8 | 9 | func pointReader(r io.Reader, byteOrder binary.ByteOrder) (geom.Geom, error) { 10 | point := geom.Point{} 11 | if err := binary.Read(r, byteOrder, &point); err != nil { 12 | return nil, err 13 | } 14 | return point, nil 15 | } 16 | 17 | func readPoints(r io.Reader, byteOrder binary.ByteOrder) ([]geom.Point, error) { 18 | var numPoints uint32 19 | if err := binary.Read(r, byteOrder, &numPoints); err != nil { 20 | return nil, err 21 | } 22 | points := make([]geom.Point, numPoints) 23 | if err := binary.Read(r, byteOrder, &points); err != nil { 24 | return nil, err 25 | } 26 | return points, nil 27 | } 28 | 29 | func writePoint(w io.Writer, byteOrder binary.ByteOrder, point geom.Point) error { 30 | return binary.Write(w, byteOrder, &point) 31 | } 32 | 33 | func writePoints(w io.Writer, byteOrder binary.ByteOrder, points []geom.Point) error { 34 | if err := binary.Write(w, byteOrder, uint32(len(points))); err != nil { 35 | return err 36 | } 37 | return binary.Write(w, byteOrder, &points) 38 | } 39 | 40 | func writePointss(w io.Writer, byteOrder binary.ByteOrder, pointss []geom.Path) error { 41 | if err := binary.Write(w, byteOrder, uint32(len(pointss))); err != nil { 42 | return err 43 | } 44 | for _, points := range pointss { 45 | if err := writePoints(w, byteOrder, points); err != nil { 46 | return err 47 | } 48 | } 49 | return nil 50 | 51 | } 52 | -------------------------------------------------------------------------------- /route/route_test.go: -------------------------------------------------------------------------------- 1 | package route 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/ctessum/geom" 8 | ) 9 | 10 | var ( 11 | link1 = geom.LineString{ 12 | geom.Point{X: 0, Y: 0}, 13 | geom.Point{X: 0, Y: 1}, 14 | geom.Point{X: 1, Y: 1}, 15 | geom.Point{X: 8, Y: 1}, 16 | geom.Point{X: 8, Y: 4}, 17 | } 18 | link3 = geom.LineString{ 19 | geom.Point{X: 7.999999999999998, Y: 4}, // floating point error 20 | geom.Point{X: 8, Y: -6}, 21 | } 22 | ) 23 | 24 | func Example() { 25 | link1 := geom.LineString{ 26 | geom.Point{X: 0, Y: 0}, 27 | geom.Point{X: 0, Y: 1}, 28 | geom.Point{X: 1, Y: 1}, 29 | geom.Point{X: 8, Y: 1}, 30 | geom.Point{X: 8, Y: 4}, 31 | } // Distance = 12 32 | link2 := geom.LineString{ 33 | geom.Point{X: 8, Y: 4}, // link2 beginning == link1 end 34 | geom.Point{X: 8, Y: -6}, 35 | } // Distance = 10 36 | startingPoint := geom.Point{X: 0, Y: -1} 37 | endingPoint := geom.Point{X: 6, Y: -6} 38 | net := NewNetwork(Time) 39 | net.AddLink(link1, 6) 40 | net.AddLink(link2, 2) 41 | route, distance, time, startDistance, endDistance := 42 | net.ShortestRoute(startingPoint, endingPoint) 43 | fmt.Println(route, distance, time, startDistance, endDistance) 44 | // Output: [[{0 0} {0 1} {1 1} {8 1} {8 4}] [{8 4} {8 -6}]] 22 7 1 2 45 | } 46 | 47 | func TestFloatingPoint(t *testing.T) { 48 | net := NewNetwork(Time) 49 | net.AddLink(link1, 6) 50 | net.AddLink(link3, 2) 51 | startingPoint := geom.Point{X: 0, Y: -1} 52 | endingPoint := geom.Point{X: 6, Y: -6} 53 | route, _, _, _, _ := net.ShortestRoute(startingPoint, endingPoint) 54 | want := "[[{0 0} {0 1} {1 1} {8 1} {8 4}] [{7.999999999999998 4} {8 -6}]]" 55 | got := fmt.Sprint(route) 56 | if want != got { 57 | t.Errorf("Want: %v; got: %v", want, got) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /point.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Point is a holder for 2D coordinates X and Y. 9 | type Point struct { 10 | X, Y float64 11 | } 12 | 13 | // NewPoint returns a new point with coordinates x and y. 14 | func NewPoint(x, y float64) *Point { 15 | return &Point{X: x, Y: y} 16 | } 17 | 18 | // Bounds gives the rectangular extents of the Point. 19 | func (p Point) Bounds() *Bounds { 20 | return NewBoundsPoint(p) 21 | } 22 | 23 | // Within calculates whether p is within poly. 24 | func (p Point) Within(poly Polygonal) WithinStatus { 25 | return pointInPolygonal(p, poly) 26 | } 27 | 28 | // Equals returns whether p is equal to p2. 29 | func (p Point) Equals(p2 Point) bool { 30 | return p.X == p2.X && p.Y == p2.Y 31 | } 32 | 33 | // Buffer returns a circle with the specified radius 34 | // centered at the receiver location. The circle is represented 35 | // as a polygon with the specified number of segments. 36 | func (p Point) Buffer(radius float64, segments int) Polygon { 37 | if segments < 3 { 38 | panic(fmt.Errorf("geom: invalid number of segments %d", segments)) 39 | } 40 | if radius < 0 { 41 | panic(fmt.Errorf("geom: invalid radius %g", radius)) 42 | } 43 | dTheta := math.Pi * 2 / float64(segments) 44 | o := make(Polygon, 1) 45 | o[0] = make([]Point, segments) 46 | for i := 0; i < segments; i++ { 47 | theta := float64(i) * dTheta 48 | o[0][i] = Point{ 49 | X: p.X + radius*math.Cos(theta), 50 | Y: p.Y + radius*math.Sin(theta), 51 | } 52 | } 53 | return o 54 | } 55 | 56 | // Len returns the number of points in the receiver (always==1). 57 | func (p Point) Len() int { return 1 } 58 | 59 | // Points returns an iterator for the points in the receiver (there will only 60 | // be one point). 61 | func (p Point) Points() func() Point { 62 | return func() Point { return p } 63 | } 64 | -------------------------------------------------------------------------------- /geom.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package geom holds geometry objects and functions to operate on them. 3 | They can be encoded and decoded by other packages in this repository.*/ 4 | package geom 5 | 6 | import "github.com/ctessum/geom/proj" 7 | 8 | // Geom is an interface for generic geometry types. 9 | type Geom interface { 10 | Bounds() *Bounds 11 | Similar(Geom, float64) bool 12 | Transform(proj.Transformer) (Geom, error) 13 | 14 | // Len returns the total number of points in the geometry 15 | Len() int 16 | 17 | // Points returns an iterator that returns the points in the 18 | // geometry. 19 | Points() func() Point 20 | } 21 | 22 | // Linear is an interface for types that are linear in nature. 23 | type Linear interface { 24 | Geom 25 | Length() float64 26 | 27 | // Clip returns the part of the line that falls within the given polygon. 28 | Clip(Polygonal) Linear 29 | 30 | Simplify(tolerance float64) Geom 31 | 32 | // Within determines whether this geometry is within the Polygonal geometry. 33 | // Points that lie on the edge of the polygon are considered within. 34 | Within(Polygonal) WithinStatus 35 | 36 | // Distance calculates the shortest distance to the given Point. 37 | Distance(Point) float64 38 | } 39 | 40 | // Polygonal is an interface for types that are polygonal in nature. 41 | type Polygonal interface { 42 | Geom 43 | Polygons() []Polygon 44 | Intersection(Polygonal) Polygonal 45 | Union(Polygonal) Polygonal 46 | XOr(Polygonal) Polygonal 47 | Difference(Polygonal) Polygonal 48 | Area() float64 49 | Simplify(tolerance float64) Geom 50 | Centroid() Point 51 | } 52 | 53 | // PointLike is an interface for types that are pointlike in nature. 54 | type PointLike interface { 55 | Geom 56 | //On(l Linear, tolerance float64) bool 57 | 58 | // Within determines whether this geometry is within the Polygonal geometry. 59 | Within(Polygonal) WithinStatus 60 | } 61 | -------------------------------------------------------------------------------- /linestring.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "math" 5 | 6 | polyclip "github.com/ctessum/polyclip-go" 7 | ) 8 | 9 | // LineString is a number of points that make up a path or line. 10 | type LineString Path 11 | 12 | // Bounds gives the rectangular extents of the LineString. 13 | func (l LineString) Bounds() *Bounds { 14 | b := NewBounds() 15 | b.extendPoints(l) 16 | return b 17 | } 18 | 19 | // Length calculates the length of l. 20 | func (l LineString) Length() float64 { 21 | length := 0. 22 | for i := 0; i < len(l)-1; i++ { 23 | p1 := l[i] 24 | p2 := l[i+1] 25 | length += math.Hypot(p2.X-p1.X, p2.Y-p1.Y) 26 | } 27 | return length 28 | } 29 | 30 | // Within calculates whether l is completely within p or touching its edge. 31 | func (l LineString) Within(p Polygonal) WithinStatus { 32 | for _, pp := range l { 33 | if pointInPolygonal(pp, p) == Outside { 34 | return Outside 35 | } 36 | } 37 | return Inside 38 | } 39 | 40 | // Distance calculates the shortest distance from p to the LineString. 41 | func (l LineString) Distance(p Point) float64 { 42 | d := math.Inf(1) 43 | for i := 0; i < len(l)-1; i++ { 44 | segDist := distPointToSegment(p, l[i], l[i+1]) 45 | d = math.Min(d, segDist) 46 | } 47 | return d 48 | } 49 | 50 | // Clip returns the part of the receiver that falls within the given polygon. 51 | func (l LineString) Clip(p Polygonal) Linear { 52 | pTemp := Polygon{Path(l)}.op(p, polyclip.CLIPLINE) 53 | o := make(MultiLineString, len(pTemp)) 54 | for i, pp := range pTemp { 55 | o[i] = LineString(pp[0 : len(pp)-1]) 56 | } 57 | return o 58 | } 59 | 60 | // Len returns the number of points in the receiver. 61 | func (l LineString) Len() int { return len(l) } 62 | 63 | // Points returns an iterator for the points in the receiver. 64 | func (l LineString) Points() func() Point { 65 | var i int 66 | return func() Point { 67 | i++ 68 | return l[i-1] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /encoding/osm/check.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/paulmach/osm" 7 | ) 8 | 9 | // Check checks OSM data to ensure that all necessary components 10 | // are present. 11 | func (o *Data) Check() error { 12 | for i, n := range o.Nodes { 13 | if n == nil { 14 | return fmt.Errorf("node %d is nil", i) 15 | } 16 | } 17 | for i, w := range o.Ways { 18 | if w == nil { 19 | return fmt.Errorf("way %d is nil", i) 20 | } 21 | for _, n := range w.Nodes { 22 | if x, ok := o.Nodes[n]; !ok { 23 | return fmt.Errorf("node %v is referenced by way %v but does not exist", 24 | n, w.ID) 25 | } else if x == nil { 26 | return fmt.Errorf("node %v is referenced by way %v but is nil", 27 | n, w.ID) 28 | } 29 | } 30 | } 31 | for i, r := range o.Relations { 32 | if r == nil { 33 | return fmt.Errorf("relation %d is nil", i) 34 | } 35 | for _, m := range r.Members { 36 | switch m.Type { 37 | case osm.TypeNode: 38 | if x, ok := o.Nodes[osm.NodeID(m.Ref)]; !ok { 39 | return fmt.Errorf("node %d is referenced by relation %d but does not exist", 40 | m.Ref, r.ID) 41 | } else if x == nil { 42 | return fmt.Errorf("node %d is referenced by relation %d but is nil", 43 | m.Ref, r.ID) 44 | } 45 | case osm.TypeWay: 46 | if x, ok := o.Ways[osm.WayID(m.Ref)]; !ok { 47 | return fmt.Errorf("way %d is referenced by relation %d but does not exist", 48 | m.Ref, r.ID) 49 | } else if x == nil { 50 | return fmt.Errorf("way %d is referenced by relation %d but is nil", 51 | m.Ref, r.ID) 52 | } 53 | case osm.TypeRelation: 54 | if x, ok := o.Relations[osm.RelationID(m.Ref)]; !ok { 55 | return fmt.Errorf("relation %d is referenced by relation %d but does not exist", 56 | m.Ref, r.ID) 57 | } else if x == nil { 58 | return fmt.Errorf("relation %d is referenced by relation %d but is nil", 59 | m.Ref, r.ID) 60 | } 61 | default: 62 | return fmt.Errorf("unknown member type %v in relation %d", m.Type, i) 63 | } 64 | } 65 | } 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /encoding/osm/tags_test.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | func TestCountTags(t *testing.T) { 11 | f, err := os.Open("testdata/honolulu_hawaii.osm.pbf") 12 | defer f.Close() 13 | if err != nil { 14 | t.Fatal(err) 15 | } 16 | tags, err := CountTags(context.Background(), f) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | if len(tags) != 19918 { 21 | t.Errorf("Wrong number of tags %d", len(tags)) 22 | } 23 | 24 | tags2 := tags.Filter(func(t *TagCount) bool { 25 | return t.Key == "highway" && t.Value == "residential" 26 | }) 27 | tableWant := [][]string{ 28 | {"Key", "Value", "Total", "Node", "Closed way", "Open way", "Relation"}, 29 | {"highway", "residential", "6839", "0", "55", "6784", "0"}} 30 | tableHave := tags2.Table() 31 | if !reflect.DeepEqual(tableWant, tableHave) { 32 | t.Error("tables don't match") 33 | } 34 | dt := (*tags2)[0].DominantType() 35 | if dt != OpenWayType { 36 | t.Errorf("dominant type should be %d but is %d", OpenWayType, dt) 37 | } 38 | } 39 | 40 | func TestData_CountTags(t *testing.T) { 41 | f, err := os.Open("testdata/honolulu_hawaii.osm.pbf") 42 | defer f.Close() 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | data, err := ExtractPBF(context.Background(), f, func(_ *Data, _ interface{}) bool { return true }, true) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | tags := data.CountTags() 51 | if len(tags) != 19918 { 52 | t.Errorf("Wrong number of tags %d", len(tags)) 53 | } 54 | 55 | tags2 := tags.Filter(func(t *TagCount) bool { 56 | return t.Key == "highway" && t.Value == "residential" 57 | }) 58 | tableWant := [][]string{ 59 | {"Key", "Value", "Total", "Node", "Closed way", "Open way", "Relation"}, 60 | {"highway", "residential", "6839", "0", "55", "6784", "0"}} 61 | tableHave := tags2.Table() 62 | if !reflect.DeepEqual(tableWant, tableHave) { 63 | t.Error("tables don't match") 64 | } 65 | dt := (*tags2)[0].DominantType() 66 | if dt != OpenWayType { 67 | t.Errorf("dominant type should be %d but is %d", OpenWayType, dt) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /encoding/shp/shp_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/ctessum/geom" 8 | ) 9 | 10 | // This example shows how to read in information from a shapefile, write 11 | // it out again, then read it in a second time to ensure that it was 12 | // properly written. 13 | func Example() { 14 | type record struct { 15 | // The geometry data will be stored here. 16 | geom.Polygon 17 | 18 | // The "value" attribute will be stored here. It would also work to 19 | // just name this field "Value". 20 | Val float64 `shp:"value"` 21 | } 22 | 23 | d, err := NewDecoder("testdata/triangles.shp") 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | e, err := NewEncoder("testdata/testout.shp", record{}) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | for { 34 | var rec record 35 | // Decode a record from the input file. 36 | if !d.DecodeRow(&rec) { 37 | break 38 | } 39 | // Encode the record to the output file 40 | if err = e.Encode(rec); err != nil { 41 | panic(err) 42 | } 43 | fmt.Printf("polygon area %.3g, value %g\n", rec.Polygon.Area(), rec.Val) 44 | } 45 | // Check to see if any errors occured during decoding. 46 | if err = d.Error(); err != nil { 47 | panic(err) 48 | } 49 | d.Close() 50 | e.Close() 51 | 52 | const testFile = "testdata/testout" 53 | // Read the data back in from the output file. 54 | d, err = NewDecoder(testFile + ".shp") 55 | if err != nil { 56 | panic(err) 57 | } 58 | for { 59 | var rec record 60 | // Decode a record from the input file. 61 | if !d.DecodeRow(&rec) { 62 | break 63 | } 64 | fmt.Printf("polygon area %.3g, value %g\n", rec.Polygon.Area(), rec.Val) 65 | } 66 | // Check to see if any errors occured during decoding. 67 | if err = d.Error(); err != nil { 68 | panic(err) 69 | } 70 | d.Close() 71 | 72 | os.Remove(testFile + ".shp") 73 | os.Remove(testFile + ".shx") 74 | os.Remove(testFile + ".dbf") 75 | 76 | // Output: 77 | // polygon area 2.3, value 6 78 | // polygon area 2.17, value 1 79 | // polygon area 2.3, value 6 80 | // polygon area 2.17, value 1 81 | } 82 | -------------------------------------------------------------------------------- /multilinestring.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "math" 5 | 6 | polyclip "github.com/ctessum/polyclip-go" 7 | ) 8 | 9 | // MultiLineString is a holder for multiple related LineStrings. 10 | type MultiLineString []LineString 11 | 12 | // Bounds gives the rectangular extents of the MultiLineString. 13 | func (ml MultiLineString) Bounds() *Bounds { 14 | b := NewBounds() 15 | for _, l := range ml { 16 | b.Extend(l.Bounds()) 17 | } 18 | return b 19 | } 20 | 21 | // Length calculates the combined length of the linestrings in ml. 22 | func (ml MultiLineString) Length() float64 { 23 | length := 0. 24 | for _, l := range ml { 25 | length += l.Length() 26 | } 27 | return length 28 | } 29 | 30 | // Within calculates whether ml is completely within p or on its edge. 31 | func (ml MultiLineString) Within(p Polygonal) WithinStatus { 32 | for _, l := range ml { 33 | if l.Within(p) == Outside { 34 | return Outside 35 | } 36 | } 37 | return Inside 38 | } 39 | 40 | // Distance calculates the shortest distance from p to the MultiLineString. 41 | func (ml MultiLineString) Distance(p Point) float64 { 42 | d := math.Inf(1) 43 | for _, l := range ml { 44 | lDist := l.Distance(p) 45 | d = math.Min(d, lDist) 46 | } 47 | return d 48 | } 49 | 50 | // Clip returns the part of the receiver that falls within the given polygon. 51 | func (ml MultiLineString) Clip(p Polygonal) Linear { 52 | pTemp := make(Polygon, len(ml)) 53 | for i, l := range ml { 54 | pTemp[i] = Path(l) 55 | } 56 | pTemp = pTemp.op(p, polyclip.CLIPLINE) 57 | o := make(MultiLineString, len(pTemp)) 58 | for i, pp := range pTemp { 59 | o[i] = LineString(pp[0 : len(pp)-1]) 60 | } 61 | return o 62 | } 63 | 64 | // Len returns the number of points in the receiver. 65 | func (ml MultiLineString) Len() int { 66 | var i int 67 | for _, l := range ml { 68 | i += len(l) 69 | } 70 | return i 71 | } 72 | 73 | // Points returns an iterator for the points in the receiver. 74 | func (ml MultiLineString) Points() func() Point { 75 | var i, j int 76 | return func() Point { 77 | if i == len(ml[j]) { 78 | j++ 79 | i = 0 80 | } 81 | i++ 82 | return ml[j][i-1] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /proj/deriveConstants.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import "math" 4 | 5 | const ( 6 | epsln = 1.0e-10 7 | // ellipoid pj_set_ell.c 8 | sixth = 0.1666666666666666667 9 | /* 1/6 */ 10 | ra4 = 0.04722222222222222222 11 | /* 17/360 */ 12 | ra6 = 0.02215608465608465608 13 | ) 14 | 15 | // DeriveConstants calculates some properties of the spatial reference based 16 | // on other properties 17 | func (json *SR) DeriveConstants() { 18 | // DGR 2011-03-20 : nagrids -> nadgrids 19 | if json.DatumCode != "" && json.DatumCode != "none" { 20 | datumDef, ok := datumDefs[json.DatumCode] 21 | if ok { 22 | json.DatumParams = make([]float64, len(datumDef.towgs84)) 23 | for i, p := range datumDef.towgs84 { 24 | json.DatumParams[i] = p 25 | } 26 | json.Ellps = datumDef.ellipse 27 | if datumDef.datumName != "" { 28 | json.DatumName = datumDef.datumName 29 | } else { 30 | json.DatumName = json.DatumCode 31 | } 32 | } 33 | } 34 | if math.IsNaN(json.A) { // do we have an ellipsoid? 35 | ellipse, ok := ellipsoidDefs[json.Ellps] 36 | if !ok { 37 | ellipse = ellipsoidDefs["WGS84"] 38 | } 39 | if ellipse.a != 0 { 40 | json.A = ellipse.a 41 | } 42 | if ellipse.b != 0 { 43 | json.B = ellipse.b 44 | } 45 | if ellipse.rf != 0 { 46 | json.Rf = ellipse.rf 47 | } 48 | json.EllipseName = ellipse.ellipseName 49 | } 50 | if !math.IsNaN(json.Rf) && math.IsNaN(json.B) { 51 | json.B = (1.0 - 1.0/json.Rf) * json.A 52 | } 53 | if json.Rf == 0 || math.Abs(json.A-json.B) < epsln { 54 | json.sphere = true 55 | json.B = json.A 56 | } 57 | json.A2 = json.A * json.A // used in geocentric 58 | json.B2 = json.B * json.B // used in geocentric 59 | json.Es = (json.A2 - json.B2) / json.A2 // e ^ 2 60 | json.E = math.Sqrt(json.Es) // eccentricity 61 | if json.Ra { 62 | json.A *= 1 - json.Es*(sixth+json.Es*(ra4+json.Es*ra6)) 63 | json.A2 = json.A * json.A 64 | json.B2 = json.B * json.B 65 | json.Es = 0 66 | } 67 | json.Ep2 = (json.A2 - json.B2) / json.B2 // used in geocentric 68 | if math.IsNaN(json.K0) { 69 | json.K0 = 1.0 //default value 70 | } 71 | //DGR 2010-11-12: axis 72 | if json.Axis == "" { 73 | json.Axis = enu 74 | } 75 | 76 | if json.datum == nil { 77 | json.datum = json.getDatum() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /carto/geojson.go: -------------------------------------------------------------------------------- 1 | package carto 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | 7 | "github.com/ctessum/geom" 8 | "github.com/ctessum/geom/encoding/geojson" 9 | ) 10 | 11 | type GeoJSONfeature struct { 12 | Type string `json:"type"` 13 | Geometry *geojson.Geometry `json:"geometry"` 14 | Properties map[string]float64 `json:"properties"` 15 | } 16 | type GeoJSON struct { 17 | Type string `json:"type"` 18 | CRS Crs `json:"crs"` 19 | Features []*GeoJSONfeature `json:"features"` 20 | } 21 | 22 | // Coordinate reference system. Used for GeoJSON 23 | type Crs struct { 24 | Type string `json:"type"` 25 | Properties CrsProps `json:"properties"` 26 | } 27 | 28 | // Coordinate reference system properties. 29 | type CrsProps struct { 30 | Name string `json:"name"` 31 | } 32 | 33 | func LoadGeoJSON(r io.Reader) (*GeoJSON, error) { 34 | out := new(GeoJSON) 35 | d := json.NewDecoder(r) 36 | err := d.Decode(&out) 37 | return out, err 38 | } 39 | 40 | func (g *GeoJSON) Sum(propertyName string) float64 { 41 | sum := 0. 42 | for _, f := range g.Features { 43 | sum += f.Properties[propertyName] 44 | } 45 | return sum 46 | } 47 | 48 | func (g *GeoJSON) GetProperty(propertyName string) []float64 { 49 | out := make([]float64, len(g.Features)) 50 | for i, f := range g.Features { 51 | out[i] = f.Properties[propertyName] 52 | } 53 | return out 54 | } 55 | 56 | func (g *GeoJSON) GetGeometry() ([]geom.Geom, error) { 57 | var err error 58 | out := make([]geom.Geom, len(g.Features)) 59 | for i, f := range g.Features { 60 | out[i], err = geojson.FromGeoJSON(f.Geometry) 61 | if err != nil { 62 | return nil, err 63 | } 64 | } 65 | return out, nil 66 | } 67 | 68 | // Convert map data to GeoJSON, where value name is a 69 | // name for the data values being output. 70 | func (m *MapData) ToGeoJSON(valueName string) (*GeoJSON, error) { 71 | var err error 72 | g := new(GeoJSON) 73 | g.Type = "FeatureCollection" 74 | g.CRS = Crs{"name", CrsProps{"EPSG:3857"}} 75 | g.Features = make([]*GeoJSONfeature, len(m.Shapes)) 76 | for i, shape := range m.Shapes { 77 | f := new(GeoJSONfeature) 78 | f.Type = "Feature" 79 | f.Geometry, err = geojson.ToGeoJSON(shape) 80 | if err != nil { 81 | return nil, err 82 | } 83 | f.Properties = map[string]float64{valueName: m.Data[i]} 84 | g.Features[i] = f 85 | } 86 | return g, nil 87 | } 88 | -------------------------------------------------------------------------------- /proj/merc.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | const ( 9 | r2d = 57.29577951308232088 10 | fortPi = math.Pi / 4 11 | ) 12 | 13 | // Merc is a mercator projection. 14 | func Merc(this *SR) (forward, inverse Transformer, err error) { 15 | if math.IsNaN(this.Long0) { 16 | this.Long0 = 0 17 | } 18 | var con = this.B / this.A 19 | Es := 1 - con*con 20 | if math.IsNaN(this.X0) { 21 | this.X0 = 0 22 | } 23 | if math.IsNaN(this.Y0) { 24 | this.Y0 = 0 25 | } 26 | E := math.Sqrt(Es) 27 | K0 := this.K0 28 | if !math.IsNaN(this.LatTS) { 29 | if this.sphere { 30 | K0 = math.Cos(this.LatTS) 31 | } else { 32 | K0 = msfnz(E, math.Sin(this.LatTS), math.Cos(this.LatTS)) 33 | } 34 | } else { 35 | if math.IsNaN(K0) { 36 | if !math.IsNaN(this.K) { 37 | K0 = this.K 38 | } else { 39 | K0 = 1 40 | } 41 | } 42 | } 43 | 44 | // Mercator forward equations--mapping lat,long to x,y 45 | forward = func(lon, lat float64) (x, y float64, err error) { 46 | // convert to radians 47 | if math.IsNaN(lat) || math.IsNaN(lon) || lat*r2d > 90 || lat*r2d < -90 || lon*r2d > 180 || lon*r2d < -180 { 48 | err = fmt.Errorf("in proj.Merc forward: invalid longitude (%g) or latitude (%g)", lon, lat) 49 | return 50 | } 51 | 52 | if math.Abs(math.Abs(lat)-halfPi) <= epsln { 53 | err = fmt.Errorf("in proj.Merc forward, abs(lat)==pi/2") 54 | return 55 | } 56 | if this.sphere { 57 | x = this.X0 + this.A*K0*adjust_lon(lon-this.Long0) 58 | y = this.Y0 + this.A*K0*math.Log(math.Tan(fortPi+0.5*lat)) 59 | } else { 60 | var sinphi = math.Sin(lat) 61 | var ts = tsfnz(this.E, lat, sinphi) 62 | x = this.X0 + this.A*K0*adjust_lon(lon-this.Long0) 63 | y = this.Y0 - this.A*K0*math.Log(ts) 64 | } 65 | return 66 | } 67 | 68 | // Mercator inverse equations--mapping x,y to lat/long 69 | inverse = func(x, y float64) (lon, lat float64, err error) { 70 | x -= this.X0 71 | y -= this.Y0 72 | 73 | if this.sphere { 74 | lat = halfPi - 2*math.Atan(math.Exp(-y/(this.A*K0))) 75 | } else { 76 | var ts = math.Exp(-y / (this.A * K0)) 77 | lat, err = phi2z(this.E, ts) 78 | if err != nil { 79 | return 80 | } 81 | } 82 | lon = adjust_lon(this.Long0 + x/(this.A*K0)) 83 | return 84 | } 85 | return 86 | } 87 | 88 | func init() { 89 | // Register this projection with the corresponding names. 90 | registerTrans(Merc, "Mercator", "Popular Visualisation Pseudo Mercator", 91 | "Mercator_1SP", "Mercator_Auxiliary_Sphere", "merc") 92 | } 93 | -------------------------------------------------------------------------------- /encoding/geojson/encode.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | 7 | "github.com/ctessum/geom" 8 | ) 9 | 10 | func pointCoordinates(point geom.Point) []float64 { 11 | return []float64{point.X, point.Y} 12 | } 13 | 14 | func pointsCoordinates(points []geom.Point) [][]float64 { 15 | coordinates := make([][]float64, len(points)) 16 | for i, point := range points { 17 | coordinates[i] = pointCoordinates(point) 18 | } 19 | return coordinates 20 | } 21 | 22 | func pointssCoordinates(pointss []geom.Path) [][][]float64 { 23 | coordinates := make([][][]float64, len(pointss)) 24 | for i, points := range pointss { 25 | coordinates[i] = pointsCoordinates(points) 26 | } 27 | return coordinates 28 | } 29 | 30 | func pointsssCoordinates(pointsss [][]geom.Path) [][][][]float64 { 31 | coordinates := make([][][][]float64, len(pointsss)) 32 | for i, points := range pointsss { 33 | coordinates[i] = pointssCoordinates(points) 34 | } 35 | return coordinates 36 | } 37 | 38 | func ToGeoJSON(g geom.Geom) (*Geometry, error) { 39 | switch g.(type) { 40 | case geom.Point: 41 | return &Geometry{ 42 | Type: "Point", 43 | Coordinates: pointCoordinates(g.(geom.Point)), 44 | }, nil 45 | case geom.MultiPoint: 46 | return &Geometry{ 47 | Type: "MultiPoint", 48 | Coordinates: pointsCoordinates(g.(geom.MultiPoint)), 49 | }, nil 50 | case geom.LineString: 51 | return &Geometry{ 52 | Type: "LineString", 53 | Coordinates: pointsCoordinates(g.(geom.LineString)), 54 | }, nil 55 | case geom.MultiLineString: 56 | lines := []geom.LineString(g.(geom.MultiLineString)) 57 | paths := make([]geom.Path, len(lines)) 58 | for i, line := range lines { 59 | paths[i] = geom.Path(line) 60 | } 61 | return &Geometry{ 62 | Type: "MultiLineString", 63 | Coordinates: pointssCoordinates(paths), 64 | }, nil 65 | case geom.Polygon: 66 | return &Geometry{ 67 | Type: "Polygon", 68 | Coordinates: pointssCoordinates(g.(geom.Polygon)), 69 | }, nil 70 | case geom.MultiPolygon: 71 | polys := []geom.Polygon(g.(geom.MultiPolygon)) 72 | pathsList := make([][]geom.Path, len(polys)) 73 | for i, poly := range polys { 74 | pathsList[i] = []geom.Path(poly) 75 | } 76 | return &Geometry{ 77 | Type: "MultiPolygon", 78 | Coordinates: pointsssCoordinates(pathsList), 79 | }, nil 80 | default: 81 | return nil, &UnsupportedGeometryError{reflect.TypeOf(g).String()} 82 | } 83 | } 84 | 85 | func Encode(g geom.Geom) ([]byte, error) { 86 | if object, err := ToGeoJSON(g); err == nil { 87 | return json.Marshal(object) 88 | } else { 89 | return nil, err 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /proj/DatumDef.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | type datumDef struct { 4 | towgs84 []float64 5 | ellipse string 6 | datumName string 7 | nadgrids []string 8 | } 9 | 10 | var datumDefs = map[string]datumDef{ 11 | "wgs84": datumDef{ 12 | towgs84: []float64{0., 0., 0.}, 13 | ellipse: "WGS84", 14 | datumName: "WGS84", 15 | }, 16 | "ch1903": datumDef{ 17 | towgs84: []float64{674.374, 15.056, 405.346}, 18 | ellipse: "bessel", 19 | datumName: "swiss", 20 | }, 21 | "ggrs87": datumDef{ 22 | towgs84: []float64{-199.87, 74.79, 246.62}, 23 | ellipse: "GRS80", 24 | datumName: "Greek_Geodetic_Reference_System_1987", 25 | }, 26 | "nad83": datumDef{ 27 | towgs84: []float64{0., 0., 0.}, 28 | ellipse: "GRS80", 29 | datumName: "North_American_Datum_1983", 30 | }, 31 | "nad27": datumDef{ 32 | nadgrids: []string{"@conus", "@alaska", "@ntv2_0.gsb", "@ntv1_can.dat"}, 33 | ellipse: "clrk66", 34 | datumName: "North_American_Datum_1927", 35 | }, 36 | "potsdam": datumDef{ 37 | towgs84: []float64{606.0, 23.0, 413.0}, 38 | ellipse: "bessel", 39 | datumName: "Potsdam Rauenberg 1950 DHDN", 40 | }, 41 | "carthage": datumDef{ 42 | towgs84: []float64{-263.0, 6.0, 431.0}, 43 | ellipse: "clark80", 44 | datumName: "Carthage 1934 Tunisia", 45 | }, 46 | "hermannskogel": datumDef{ 47 | towgs84: []float64{653.0, -212.0, 449.0}, 48 | ellipse: "bessel", 49 | datumName: "Hermannskogel", 50 | }, 51 | "ire65": datumDef{ 52 | towgs84: []float64{482.530, -130.596, 564.557, -1.042, -0.214, -0.631, 8.15}, 53 | ellipse: "mod_airy", 54 | datumName: "Ireland 1965", 55 | }, 56 | "rassadiran": datumDef{ 57 | towgs84: []float64{-133.63, -157.5, -158.62}, 58 | ellipse: "intl", 59 | datumName: "Rassadiran", 60 | }, 61 | "nzgd49": datumDef{ 62 | towgs84: []float64{59.47, -5.04, 187.44, 0.47, -0.1, 1.024, -4.5993}, 63 | ellipse: "intl", 64 | datumName: "New Zealand Geodetic Datum 1949", 65 | }, 66 | "osgb36": datumDef{ 67 | towgs84: []float64{446.448, -125.157, 542.060, 0.1502, 0.2470, 0.8421, -20.4894}, 68 | ellipse: "airy", 69 | datumName: "Airy 1830", 70 | }, 71 | "s_jtsk": datumDef{ 72 | towgs84: []float64{589, 76, 480}, 73 | ellipse: "bessel", 74 | datumName: "S-JTSK (Ferro)", 75 | }, 76 | "beduaram": datumDef{ 77 | towgs84: []float64{-106, -87, 188}, 78 | ellipse: "clrk80", 79 | datumName: "Beduaram", 80 | }, 81 | "gunung_segara": datumDef{ 82 | towgs84: []float64{-403, 684, 41}, 83 | ellipse: "bessel", 84 | datumName: "Gunung Segara Jakarta", 85 | }, 86 | "rnb72": datumDef{ 87 | towgs84: []float64{106.869, -52.2978, 103.724, -0.33657, 0.456955, -1.84218, 1}, 88 | ellipse: "intl", 89 | datumName: "Reseau National Belge 1972", 90 | }, 91 | } 92 | -------------------------------------------------------------------------------- /carto/map_test.go: -------------------------------------------------------------------------------- 1 | package carto 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | "image/color" 6 | "math/rand" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestMap(t *testing.T) { 12 | shape := geom.Geom(geom.Polygon([]geom.Path{[]geom.Point{ 13 | {1., 0.}, {2., 1.}, {1., 2.}, 14 | {0., 1.}, {1., 0.}}})) 15 | f, err := os.Create("test.png") 16 | if err != nil { 17 | panic(err) 18 | } 19 | DrawShapes(f, []color.NRGBA{{0, 0, 0, 255}}, 20 | []color.NRGBA{{0, 255, 0, 127}}, 5, 0, shape) 21 | f.Close() 22 | } 23 | 24 | func TestCmapPos(t *testing.T) { 25 | x := make([]float64, 100) 26 | for i := 0; i < len(x); i++ { 27 | x[i] = rand.Float64() 28 | } 29 | c := NewColorMap(LinCutoff) 30 | c.AddArray(x) 31 | c.Set() 32 | f, err := os.Create("legendTestPos.pdf") 33 | if err != nil { 34 | panic(err) 35 | } 36 | canvas := NewDefaultLegendCanvas() 37 | err = c.Legend(&canvas.Canvas, "Units!") 38 | if err != nil { 39 | panic(err) 40 | } 41 | err = canvas.WriteTo(f) 42 | if err != nil { 43 | panic(err) 44 | } 45 | f.Close() 46 | } 47 | 48 | func TestCmapNeg(t *testing.T) { 49 | x := make([]float64, 100) 50 | for i := 0; i < len(x); i++ { 51 | x[i] = rand.Float64() - 1 52 | } 53 | c := NewColorMap(LinCutoff) 54 | c.AddArray(x) 55 | c.Set() 56 | f, err := os.Create("legendTestNeg.pdf") 57 | if err != nil { 58 | panic(err) 59 | } 60 | canvas := NewDefaultLegendCanvas() 61 | err = c.Legend(&canvas.Canvas, "Units!") 62 | if err != nil { 63 | panic(err) 64 | } 65 | err = canvas.WriteTo(f) 66 | if err != nil { 67 | panic(err) 68 | } 69 | f.Close() 70 | } 71 | 72 | func TestCmapPosNeg(t *testing.T) { 73 | x := make([]float64, 100) 74 | for i := 0; i < len(x); i++ { 75 | x[i] = (rand.Float64() - 0.5) * 64. 76 | } 77 | c := NewColorMap(LinCutoff) 78 | c.AddArray(x) 79 | c.Set() 80 | f, err := os.Create("legendTestPosNeg.pdf") 81 | if err != nil { 82 | panic(err) 83 | } 84 | canvas := NewDefaultLegendCanvas() 85 | err = c.Legend(&canvas.Canvas, "Units!") 86 | if err != nil { 87 | panic(err) 88 | } 89 | err = canvas.WriteTo(f) 90 | if err != nil { 91 | panic(err) 92 | } 93 | f.Close() 94 | } 95 | 96 | func TestInterpolate(t *testing.T) { 97 | c := Optimized.interpolate(0) 98 | white := color.NRGBA{255, 255, 255, 255} 99 | if c != white { 100 | t.Errorf("Color %v should be white", c) 101 | } 102 | c = Optimized.interpolate(-1) 103 | blue := color.NRGBA{59, 76, 192, 255} 104 | if c != blue { 105 | t.Errorf("Color %v should be 59,76,192", c) 106 | } 107 | c = Optimized.interpolate(1) 108 | red := color.NRGBA{180, 4, 38, 255} 109 | if c != red { 110 | t.Errorf("Color %v should be 180,4,38", c) 111 | } 112 | c = Optimized.interpolate(-0.92) 113 | blue2 := color.NRGBA{71, 94, 207, 255} 114 | if c != blue2 { 115 | t.Errorf("Color %v should be 71,94,207", c) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /proj/eqdc.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // EqdC is an Equidistant Conic projection. 9 | func EqdC(this *SR) (forward, inverse Transformer, err error) { 10 | // Standard Parallels cannot be equal and on opposite sides of the equator 11 | if math.Abs(this.Lat1+this.Lat2) < epsln { 12 | return nil, nil, fmt.Errorf("proj: Equidistant Conic parallels cannot be equal and on opposite sides of the equator but are %g and %g", this.Lat1, this.Lat2) 13 | } 14 | if math.IsNaN(this.Lat2) { 15 | this.Lat2 = this.Lat1 16 | } 17 | 18 | temp := this.B / this.A 19 | this.Es = 1 - math.Pow(temp, 2) 20 | this.E = math.Sqrt(this.Es) 21 | e0 := e0fn(this.Es) 22 | e1 := e1fn(this.Es) 23 | e2 := e2fn(this.Es) 24 | e3 := e3fn(this.Es) 25 | 26 | sinphi := math.Sin(this.Lat1) 27 | cosphi := math.Cos(this.Lat1) 28 | 29 | ms1 := msfnz(this.E, sinphi, cosphi) 30 | ml1 := mlfn(e0, e1, e2, e3, this.Lat1) 31 | 32 | var ns float64 33 | if math.Abs(this.Lat1-this.Lat2) < epsln { 34 | ns = sinphi 35 | } else { 36 | sinphi = math.Sin(this.Lat2) 37 | cosphi = math.Cos(this.Lat2) 38 | ms2 := msfnz(this.E, sinphi, cosphi) 39 | ml2 := mlfn(e0, e1, e2, e3, this.Lat2) 40 | ns = (ms1 - ms2) / (ml2 - ml1) 41 | } 42 | g := ml1 + ms1/ns 43 | ml0 := mlfn(e0, e1, e2, e3, this.Lat0) 44 | rh := this.A * (g - ml0) 45 | 46 | /* Equidistant Conic forward equations--mapping lat,long to x,y 47 | -----------------------------------------------------------*/ 48 | forward = func(lon, lat float64) (x, y float64, err error) { 49 | var rh1 float64 50 | 51 | /* Forward equations 52 | -----------------*/ 53 | if this.sphere { 54 | rh1 = this.A * (g - lat) 55 | } else { 56 | var ml = mlfn(e0, e1, e2, e3, lat) 57 | rh1 = this.A * (g - ml) 58 | } 59 | var theta = ns * adjust_lon(lon-this.Long0) 60 | x = this.X0 + rh1*math.Sin(theta) 61 | y = this.Y0 + rh - rh1*math.Cos(theta) 62 | return x, y, nil 63 | } 64 | 65 | /* Inverse equations 66 | -----------------*/ 67 | inverse = func(x, y float64) (lon, lat float64, err error) { 68 | x -= this.X0 69 | y = rh - y + this.Y0 70 | var con, rh1 float64 71 | if ns >= 0 { 72 | rh1 = math.Sqrt(x*x + y*y) 73 | con = 1 74 | } else { 75 | rh1 = -math.Sqrt(x*x + y*y) 76 | con = -1 77 | } 78 | var theta float64 79 | if rh1 != 0 { 80 | theta = math.Atan2(con*x, con*y) 81 | } 82 | 83 | if this.sphere { 84 | lon = adjust_lon(this.Long0 + theta/ns) 85 | lat = adjust_lat(g - rh1/this.A) 86 | return 87 | } 88 | var ml = g - rh1/this.A 89 | lat, err = imlfn(ml, e0, e1, e2, e3) 90 | if err != nil { 91 | return math.NaN(), math.NaN(), err 92 | } 93 | lon = adjust_lon(this.Long0 + theta/ns) 94 | return 95 | } 96 | return 97 | } 98 | 99 | func init() { 100 | registerTrans(EqdC, "Equidistant_Conic", "eqdc") 101 | } 102 | -------------------------------------------------------------------------------- /encoding/wkb/wkb_test.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "github.com/ctessum/geom" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestWKB(t *testing.T) { 10 | 11 | var testCases = []struct { 12 | g geom.Geom 13 | xdr []byte 14 | ndr []byte 15 | }{ 16 | { 17 | g: geom.Point{X: 1, Y: 2}, 18 | xdr: []byte("\x00\x00\x00\x00\x01?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00"), 19 | ndr: []byte("\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@"), 20 | }, 21 | { 22 | g: geom.LineString([]geom.Point{{1, 2}, {3, 4}}), 23 | xdr: []byte("\x00\x00\x00\x00\x02\x00\x00\x00\x02?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@\x08\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00"), 24 | ndr: []byte("\x01\x02\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@"), 25 | }, 26 | { 27 | g: geom.Polygon([]geom.Path{{{1, 2}, {3, 4}, {5, 6}, {1, 2}}}), 28 | xdr: []byte("\x00\x00\x00\x00\x03\x00\x00\x00\x01\x00\x00\x00\x04?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00@\x08\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00@\x14\x00\x00\x00\x00\x00\x00@\x18\x00\x00\x00\x00\x00\x00?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00"), 29 | ndr: []byte("\x01\x03\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@\x00\x00\x00\x00\x00\x00\x14@\x00\x00\x00\x00\x00\x00\x18@\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@"), 30 | }, 31 | { 32 | g: geom.MultiPoint([]geom.Point{{1, 2}, {3, 4}}), 33 | xdr: []byte("\x00\x00\x00\x00\x04\x00\x00\x00\x02\x00\x00\x00\x00\x01?\xf0\x00\x00\x00\x00\x00\x00@\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01@\x08\x00\x00\x00\x00\x00\x00@\x10\x00\x00\x00\x00\x00\x00"), 34 | ndr: []byte("\x01\x04\x00\x00\x00\x02\x00\x00\x00\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\xf0?\x00\x00\x00\x00\x00\x00\x00@\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x08@\x00\x00\x00\x00\x00\x00\x10@"), 35 | }, 36 | } 37 | 38 | for _, tc := range testCases { 39 | 40 | // test XDR decoding 41 | if got, err := Decode(tc.xdr); err != nil || !reflect.DeepEqual(got, tc.g) { 42 | t.Errorf("Decode(%#v) == %#v, %s, want %#v, nil", tc.xdr, got, err, tc.g) 43 | } 44 | 45 | // test XDR encoding 46 | if got, err := Encode(tc.g, XDR); err != nil || !reflect.DeepEqual(got, tc.xdr) { 47 | t.Errorf("Encode(%#v, %#v) == %#v, %#v, want %#v, nil", tc.g, XDR, got, err, tc.xdr) 48 | } 49 | 50 | // test NDR decoding 51 | if got, err := Decode(tc.ndr); err != nil || !reflect.DeepEqual(got, tc.g) { 52 | t.Errorf("Decode(%#v) == %#v, %s, want %#v, nil", tc.ndr, got, err, tc.g) 53 | } 54 | 55 | // test NDR encoding 56 | if got, err := Encode(tc.g, NDR); err != nil || !reflect.DeepEqual(got, tc.ndr) { 57 | t.Errorf("Encode(%#v, %#v) == %#v, %#v, want %#v, nil", tc.g, NDR, got, err, tc.ndr) 58 | } 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /within.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "math" 4 | 5 | // Withiner is an interface for types that can be determined to be 6 | // within a polygon or not. 7 | type Withiner interface { 8 | Within(Polygonal) WithinStatus 9 | } 10 | 11 | // pointInPolygonal determines whether "pt" is 12 | // within any of the polygons in "pg". 13 | // adapted from https://rosettacode.org/wiki/Ray-casting_algorithm#Go. 14 | // In this version of the algorithm, points that lie on the edge of the polygon 15 | // are considered inside. 16 | func pointInPolygonal(pt Point, pg Polygonal) (in WithinStatus) { 17 | for _, poly := range pg.Polygons() { 18 | pgBounds := poly.ringBounds() 19 | tempIn := pointInPolygon(pt, poly, pgBounds) 20 | if tempIn == OnEdge { 21 | return tempIn 22 | } else if tempIn == Inside { 23 | in = in.invert() 24 | } 25 | } 26 | return in 27 | } 28 | 29 | // WithinStatus gives the status of a point relative to a polygon: whether 30 | // it is inside, outside, or on the edge. 31 | type WithinStatus int 32 | 33 | // WithinStatus gives the status of a point relative to a polygon: whether 34 | // it is inside, outside, or on the edge. 35 | const ( 36 | Outside WithinStatus = iota 37 | Inside 38 | OnEdge 39 | ) 40 | 41 | func (w WithinStatus) invert() WithinStatus { 42 | if w == Outside { 43 | return Inside 44 | } 45 | return Outside 46 | } 47 | 48 | // pointInPolygon determines whether "pt" is 49 | // within "pg". 50 | // adapted from https://rosettacode.org/wiki/Ray-casting_algorithm#Go. 51 | // pgBounds is the bounds of each ring in pg. 52 | func pointInPolygon(pt Point, pg Polygon, pgBounds []*Bounds) (in WithinStatus) { 53 | for i, ring := range pg { 54 | if len(ring) < 3 { 55 | continue 56 | } 57 | if !pgBounds[i].Overlaps(NewBoundsPoint(pt)) { 58 | continue 59 | } 60 | // check segment between beginning and ending points 61 | if !ring[len(ring)-1].Equals(ring[0]) { 62 | if pointOnSegment(pt, ring[len(ring)-1], ring[0]) { 63 | return OnEdge 64 | } 65 | if rayIntersectsSegment(pt, ring[len(ring)-1], ring[0]) { 66 | in = in.invert() 67 | } 68 | } 69 | // check the rest of the segments. 70 | for i := 1; i < len(ring); i++ { 71 | if pointOnSegment(pt, ring[i-1], ring[i]) { 72 | return OnEdge 73 | } 74 | if rayIntersectsSegment(pt, ring[i-1], ring[i]) { 75 | in = in.invert() 76 | } 77 | } 78 | } 79 | return in 80 | } 81 | 82 | func rayIntersectsSegment(p, a, b Point) bool { 83 | if a.Y > b.Y { 84 | a, b = b, a 85 | } 86 | for p.Y == a.Y || p.Y == b.Y { 87 | p.Y = math.Nextafter(p.Y, math.Inf(1)) 88 | } 89 | if p.Y < a.Y || p.Y > b.Y { 90 | return false 91 | } 92 | if a.X > b.X { 93 | if p.X >= a.X { 94 | return false 95 | } 96 | if p.X < b.X { 97 | return true 98 | } 99 | } else { 100 | if p.X > b.X { 101 | return false 102 | } 103 | if p.X < a.X { 104 | return true 105 | } 106 | } 107 | return (p.Y-a.Y)/(p.X-a.X) >= (b.Y-a.Y)/(b.X-a.X) 108 | } 109 | -------------------------------------------------------------------------------- /encoding/osm/keep.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/ctessum/geom" 7 | "github.com/paulmach/osm" 8 | ) 9 | 10 | // KeepFunc is a function that determines whether an OSM object should 11 | // be included in the output. The object may be either *osmpbf.Node, 12 | // *osmpbf.Way, or *osmpbf.Relation 13 | type KeepFunc func(d *Data, object interface{}) bool 14 | 15 | // KeepTags keeps OSM objects that contain the given tag key 16 | // with at least one of the given tag values, where 17 | // the keys and values correspond to the keys and values 18 | // of the 'tags' input. If no 19 | // tag valuse are given, then all object with the given 20 | // key will be kept. 21 | func KeepTags(tags map[string][]string) KeepFunc { 22 | return func(_ *Data, object interface{}) bool { 23 | switch object.(type) { 24 | case *osm.Node: 25 | return hasTag(object.(*osm.Node).Tags, tags) 26 | case *Node: 27 | return hasTag(object.(*Node).Tags, tags) 28 | case *osm.Way: 29 | return hasTag(object.(*osm.Way).Tags, tags) 30 | case *Way: 31 | return hasTag(object.(*Way).Tags, tags) 32 | case *osm.Relation: 33 | return hasTag(object.(*osm.Relation).Tags, tags) 34 | case *Relation: 35 | return hasTag(object.(*Relation).Tags, tags) 36 | default: 37 | panic(fmt.Errorf("osm: invalid object type %T", object)) 38 | } 39 | } 40 | } 41 | 42 | // KeepBounds keeps OSM objects that overlap with b. 43 | // Using KeepBounds in combination with other KeepFuncs may 44 | // result in unexpected results. 45 | func KeepBounds(b *geom.Bounds) KeepFunc { 46 | return func(o *Data, object interface{}) bool { 47 | switch object.(type) { 48 | case *osm.Node: 49 | // For nodes, keep anything that is within b. 50 | n := object.(*osm.Node) 51 | return b.Overlaps(geom.Point{X: n.Lon, Y: n.Lat}.Bounds()) 52 | case *osm.Way: 53 | // For ways, keep anything that requires a node that we're already keeping. 54 | w := object.(*osm.Way) 55 | for _, n := range w.Nodes { 56 | if has, _ := o.hasNeedNode(n.ID); has { 57 | return true 58 | } 59 | } 60 | case *osm.Relation: 61 | // For relations, keep anything that requires a node, way or relation that 62 | // we're already keeping. 63 | r := object.(*osm.Relation) 64 | for _, m := range r.Members { 65 | switch m.Type { 66 | case osm.TypeNode: 67 | if has, _ := o.hasNeedNode(osm.NodeID(m.Ref)); has { 68 | return true 69 | } 70 | case osm.TypeWay: 71 | if has, _ := o.hasNeedWay(osm.WayID(m.Ref)); has { 72 | return true 73 | } 74 | case osm.TypeRelation: 75 | if has, _ := o.hasNeedRelation(osm.RelationID(m.Ref)); has { 76 | return true 77 | } 78 | default: 79 | panic(fmt.Errorf("unknown member type %v", m.Type)) 80 | } 81 | } 82 | default: 83 | panic(fmt.Errorf("osm: invalid object type %T", object)) 84 | } 85 | return false 86 | } 87 | } 88 | 89 | // KeepAll specifies that all objects should be kept. 90 | func KeepAll() KeepFunc { 91 | return func(_ *Data, _ interface{}) bool { return true } 92 | } 93 | -------------------------------------------------------------------------------- /within_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "testing" 4 | 5 | func TestWithin1(t *testing.T) { 6 | p := Point{620858.7034230313, -1.3334340701764394e+06} 7 | b := Polygon{ 8 | []Point{ 9 | {-2.758081092115788e+06, -2.1035219712004187e+06}, 10 | {-2.7580810921157864e+06, 1.9603377468041454e+06}, 11 | {2.6080741578387334e+06, 1.954523927465083e+06}, 12 | {2.60226033849967e+06, -2.10352197120042e+06}, 13 | {-2.758081092115788e+06, -2.1035219712004187e+06}, 14 | }, 15 | } 16 | if p.Within(b) != Inside { 17 | t.Errorf("Point %v should be within polygon %v", p, b) 18 | } 19 | } 20 | 21 | // Adapted from https://rosettacode.org/wiki/Ray-casting_algorithm#Go 22 | func TestWithin2(t *testing.T) { 23 | var ( 24 | p1 = Point{0, 0} 25 | p2 = Point{10, 0} 26 | p3 = Point{10, 10} 27 | p4 = Point{0, 10} 28 | p5 = Point{2.5, 2.5} 29 | p6 = Point{7.5, 2.5} 30 | p7 = Point{7.5, 7.5} 31 | p8 = Point{2.5, 7.5} 32 | p9 = Point{0, 5} 33 | p10 = Point{10, 5} 34 | p11 = Point{3, 0} 35 | p12 = Point{7, 0} 36 | p13 = Point{7, 10} 37 | p14 = Point{3, 10} 38 | ) 39 | type poly struct { 40 | name string 41 | sides Polygon 42 | results []WithinStatus 43 | } 44 | 45 | var tpg = []poly{ 46 | poly{ 47 | name: "square", 48 | sides: Polygon{[]Point{p1, p2, p3, p4, p1}}, 49 | results: []WithinStatus{Inside, Inside, Outside, OnEdge, OnEdge, Inside, OnEdge, Inside, Inside}, 50 | }, 51 | poly{ 52 | name: "square hole", 53 | sides: Polygon{ 54 | []Point{p1, p2, p3, p4, p1}, 55 | []Point{p5, p6, p7, p8, p5}, 56 | }, 57 | results: []WithinStatus{Outside, Inside, Outside, OnEdge, OnEdge, Inside, OnEdge, Inside, Inside}, 58 | }, 59 | poly{ 60 | name: "strange", 61 | sides: Polygon{[]Point{p1, p5, p4, p8, p7, p3, p2, p5}}, 62 | results: []WithinStatus{Inside, Outside, Outside, Outside, OnEdge, Inside, OnEdge, Outside, Outside}, 63 | }, 64 | poly{ 65 | name: "exagon", 66 | sides: Polygon{[]Point{p11, p12, p10, p13, p14, p9, p11}}, 67 | results: []WithinStatus{Inside, Inside, Outside, OnEdge, OnEdge, Inside, Outside, Outside, Outside}, 68 | }, 69 | } 70 | 71 | var tpt = []Point{{5, 5}, {5, 8}, {-10, 5}, {0, 5}, {10, 5}, {8, 5}, {10, 10}, {1, 2}, {2, 1}} 72 | 73 | for _, pg := range tpg { 74 | for i, pt := range tpt { 75 | if pg.results[i] != pt.Within(pg.sides) { 76 | t.Errorf("point %v within polygon %v should be %v", pt, pg.name, pg.results[i]) 77 | } 78 | } 79 | } 80 | } 81 | 82 | func TestWithin3(t *testing.T) { 83 | p := Point{X: 2, Y: 2} 84 | poly := Polygon{{ 85 | Point{X: 1, Y: 0}, 86 | Point{X: 2, Y: 2}, 87 | Point{X: 0, Y: 2}, 88 | }} 89 | if p.Within(poly) != OnEdge { 90 | t.Errorf("%v should be on edge of %v", p, poly) 91 | } 92 | } 93 | 94 | func TestWithin4(t *testing.T) { 95 | p := Point{X: 1, Y: 0.5} 96 | poly := Polygon{{ 97 | Point{X: 0, Y: 1}, 98 | Point{X: 1, Y: 0}, 99 | Point{X: 2, Y: 1}, 100 | Point{X: 1, Y: 2}, 101 | }} 102 | if p.Within(poly) != Inside { 103 | t.Errorf("%v should be within %v", p, poly) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /proj/common.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | func msfnz(eccent, sinphi, cosphi float64) float64 { 9 | var con = eccent * sinphi 10 | return cosphi / (math.Sqrt(1 - con*con)) 11 | } 12 | 13 | func sign(x float64) float64 { 14 | if x < 0 { 15 | return -1 16 | } 17 | return 1 18 | } 19 | 20 | const ( 21 | twoPi = math.Pi * 2 22 | // SPI is slightly greater than Math.PI, so values that exceed the -180..180 23 | // degree range by a tiny amount don't get wrapped. This prevents points that 24 | // have drifted from their original location along the 180th meridian (due to 25 | // floating point error) from changing their sign. 26 | sPi = 3.14159265359 27 | halfPi = math.Pi / 2 28 | ) 29 | 30 | func adjust_lon(x float64) float64 { 31 | if math.Abs(x) <= sPi { 32 | return x 33 | } 34 | return (x - (sign(x) * twoPi)) 35 | } 36 | 37 | func adjust_lat(x float64) float64 { 38 | if math.Abs(x) < halfPi { 39 | return x 40 | } 41 | return (x - (sign(x) * math.Pi)) 42 | } 43 | 44 | func tsfnz(eccent, phi, sinphi float64) float64 { 45 | var con = eccent * sinphi 46 | var com = 0.5 * eccent 47 | con = math.Pow(((1 - con) / (1 + con)), com) 48 | return (math.Tan(0.5*(halfPi-phi)) / con) 49 | } 50 | 51 | func phi2z(eccent, ts float64) (float64, error) { 52 | var eccnth = 0.5 * eccent 53 | phi := halfPi - 2*math.Atan(ts) 54 | for i := 0; i <= 15; i++ { 55 | con := eccent * math.Sin(phi) 56 | dphi := halfPi - 2*math.Atan(ts*(math.Pow(((1-con)/(1+con)), eccnth))) - phi 57 | phi += dphi 58 | if math.Abs(dphi) <= 0.0000000001 { 59 | return phi, nil 60 | } 61 | } 62 | return math.NaN(), fmt.Errorf("phi2z has no convergence") 63 | } 64 | 65 | func e0fn(x float64) float64 { 66 | return (1 - 0.25*x*(1+x/16*(3+1.25*x))) 67 | } 68 | 69 | func e1fn(x float64) float64 { 70 | return (0.375 * x * (1 + 0.25*x*(1+0.46875*x))) 71 | } 72 | 73 | func e2fn(x float64) float64 { 74 | return (0.05859375 * x * x * (1 + 0.75*x)) 75 | } 76 | 77 | func e3fn(x float64) float64 { 78 | return (x * x * x * (35 / 3072)) 79 | } 80 | 81 | func mlfn(e0, e1, e2, e3, phi float64) float64 { 82 | return (e0*phi - e1*math.Sin(2*phi) + e2*math.Sin(4*phi) - e3*math.Sin(6*phi)) 83 | } 84 | 85 | func asinz(x float64) float64 { 86 | if math.Abs(x) > 1 { 87 | if x > 1 { 88 | x = 1 89 | } else { 90 | x = -1 91 | } 92 | } 93 | return math.Asin(x) 94 | } 95 | 96 | func qsfnz(eccent, sinphi float64) float64 { 97 | var con float64 98 | if eccent > 1.0e-7 { 99 | con = eccent * sinphi 100 | return ((1 - eccent*eccent) * (sinphi/(1-con*con) - (0.5/eccent)*math.Log((1-con)/(1+con)))) 101 | } else { 102 | return (2 * sinphi) 103 | } 104 | } 105 | 106 | func imlfn(ml, e0, e1, e2, e3 float64) (float64, error) { 107 | phi := ml / e0 108 | for i := 0; i < 15; i++ { 109 | dphi := (ml - (e0*phi - e1*math.Sin(2*phi) + e2*math.Sin(4*phi) - e3*math.Sin(6*phi))) / (e0 - 2*e1*math.Cos(2*phi) + 4*e2*math.Cos(4*phi) - 6*e3*math.Cos(6*phi)) 110 | phi += dphi 111 | if math.Abs(dphi) <= 0.0000000001 { 112 | return phi, nil 113 | } 114 | } 115 | return math.NaN(), fmt.Errorf("proj: imlfn: Latitude failed to converge after 15 iterations") 116 | } 117 | -------------------------------------------------------------------------------- /encoding/geojson/geojson_test.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/ctessum/geom" 8 | ) 9 | 10 | func TestGeoJSON(t *testing.T) { 11 | testCases := []struct { 12 | g geom.Geom 13 | geoJSON []byte 14 | }{ 15 | { 16 | geom.Point{1, 2}, 17 | []byte(`{"type":"Point","coordinates":[1,2]}`), 18 | }, 19 | { 20 | geom.MultiPoint{ 21 | geom.Point{1, 2}, 22 | geom.Point{3, 4}, 23 | }, 24 | []byte(`{"type":"MultiPoint","coordinates":[[1,2],[3,4]]}`), 25 | }, 26 | { 27 | geom.LineString(geom.Path{{1, 2}, {3, 4}}), 28 | []byte(`{"type":"LineString","coordinates":[[1,2],[3,4]]}`), 29 | }, 30 | { 31 | geom.MultiLineString{ 32 | geom.LineString(geom.Path{{1, 2}, {3, 4}}), 33 | geom.LineString(geom.Path{{5, 6}, {7, 8}}), 34 | }, 35 | []byte(`{"type":"MultiLineString","coordinates":[[[1,2],[3,4]],[[5,6],[7,8]]]}`), 36 | }, 37 | { 38 | geom.Polygon([]geom.Path{{{1, 2}, {3, 4}, {5, 6}}}), 39 | []byte(`{"type":"Polygon","coordinates":[[[1,2],[3,4],[5,6]]]}`), 40 | }, 41 | { 42 | geom.MultiPolygon{ 43 | geom.Polygon([]geom.Path{{{1, 2}, {3, 4}, {5, 6}}}), 44 | geom.Polygon([]geom.Path{{{7, 8}, {9, 10}, {11, 12}}}), 45 | }, 46 | []byte(`{"type":"MultiPolygon","coordinates":[[[[1,2],[3,4],[5,6]]],[[[7,8],[9,10],[11,12]]]]}`), 47 | }, 48 | } 49 | for _, tc := range testCases { 50 | if got, err := Encode(tc.g); err != nil || !reflect.DeepEqual(got, tc.geoJSON) { 51 | t.Errorf("Encode(%#v) == %#v, %#v, want %#v, nil", tc.g, got, err, tc.geoJSON) 52 | } 53 | if got, err := Decode(tc.geoJSON); err != nil || !reflect.DeepEqual(got, tc.g) { 54 | t.Errorf("Decode(%#v) == %#v, %#v, want %#v, nil", tc.geoJSON, got, err, tc.g) 55 | } 56 | } 57 | } 58 | 59 | func TestGeoJSONDecode(t *testing.T) { 60 | testCases := [][]byte{ 61 | []byte(`{}`), 62 | []byte(`{"type":""}`), 63 | []byte(`{"type":"Point"}`), 64 | []byte(`{"coordinates":[],"type":"Point"}`), 65 | []byte(`{"coordinates":[1],"type":"Point"}`), 66 | []byte(`{"coordinates":[1,2,3,4],"type":"Point"}`), 67 | []byte(`{"coordinates":[""],"type":"Point"}`), 68 | []byte(`{"type":"MultiPoint"}`), 69 | []byte(`{"coordinates":[1,2],type":"MultiPoint"}`), 70 | []byte(`{"type":"LineString"}`), 71 | []byte(`{"coordinates":[],"type":"LineString"}`), 72 | []byte(`{"coordinates":[[]],"type":"LineString"}`), 73 | []byte(`{"coordinates":[1],"type":"LineString"}`), 74 | []byte(`{"coordinates":[[1,2],[3,4,5]],"type":"LineString"}`), 75 | []byte(`{"coordinates":[""],"type":"LineString"}`), 76 | []byte(`{"coordinates":[[1,2,3,4],[5,6,7,8]],"type":"LineString"}`), 77 | []byte(`{"type":"MultiLineString"}`), 78 | []byte(`{"coordinates":[[1,2,3,4],[5,6,7,8]],"type":"MultiLineString"}`), 79 | []byte(`{"type":"Polygon"}`), 80 | []byte(`{"coordinates":[],"type":"Polygon"}`), 81 | []byte(`{"coordinates":[[]],"type":"Polygon"}`), 82 | []byte(`{"coordinates":[[[]]],"type":"Polygon"}`), 83 | []byte(`{"coordinates":[[[1,2],[3,4,5]]],"type":"Polygon"}`), 84 | []byte(`{"type":"MultiPolygon"}`), 85 | []byte(`{"coordinates":[[[1,2],[3,4,5]]],"type":"MultiPolygon"}`), 86 | } 87 | for _, tc := range testCases { 88 | if got, err := Decode(tc); err == nil { 89 | t.Errorf("Decode(%#v) == %#v, nil, want err != nil", tc, got) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /polygon.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/ctessum/polyclip-go" 7 | ) 8 | 9 | // A Path is a series of connected points. 10 | type Path []Point 11 | 12 | // Len returns the number of Points in the receiver. 13 | func (p Path) Len() int { 14 | return len(p) 15 | } 16 | 17 | // XY returns the coordinates of point i. 18 | func (p Path) XY(i int) (x, y float64) { 19 | return p[i].X, p[i].Y 20 | } 21 | 22 | // A Polygon is a series of closed rings. The inner rings should be nested 23 | // inside of the outer ring. 24 | type Polygon []Path 25 | 26 | // Bounds gives the rectangular extents of the polygon. 27 | func (p Polygon) Bounds() *Bounds { 28 | b := NewBounds() 29 | b.extendPointss(p) 30 | return b 31 | } 32 | 33 | // Polygons returns []{p} to fulfill the Polygonal interface. 34 | func (p Polygon) Polygons() []Polygon { 35 | return []Polygon{p} 36 | } 37 | 38 | // Intersection returns the area(s) shared by p and p2. 39 | func (p Polygon) Intersection(p2 Polygonal) Polygonal { 40 | return p.op(p2, polyclip.INTERSECTION) 41 | } 42 | 43 | // Union returns the combination of p and p2. 44 | func (p Polygon) Union(p2 Polygonal) Polygonal { 45 | return p.op(p2, polyclip.UNION) 46 | } 47 | 48 | // XOr returns the area(s) occupied by either p or p2 but not both. 49 | func (p Polygon) XOr(p2 Polygonal) Polygonal { 50 | return p.op(p2, polyclip.XOR) 51 | } 52 | 53 | // Difference subtracts p2 from p. 54 | func (p Polygon) Difference(p2 Polygonal) Polygonal { 55 | return p.op(p2, polyclip.DIFFERENCE) 56 | } 57 | 58 | func (p Polygon) op(p2 Polygonal, op polyclip.Op) Polygon { 59 | pp := p.toPolyClip() 60 | var pp2 polyclip.Polygon 61 | for _, pp2x := range p2.Polygons() { 62 | pp2 = append(pp2, pp2x.toPolyClip()...) 63 | } 64 | return polyClipToPolygon(pp.Construct(op, pp2)) 65 | } 66 | 67 | func (p Polygon) toPolyClip() polyclip.Polygon { 68 | o := make(polyclip.Polygon, len(p)) 69 | for i, r := range p { 70 | o[i] = make(polyclip.Contour, len(r)) 71 | for j, pp := range r { 72 | o[i][j] = polyclip.Point(pp) 73 | } 74 | } 75 | return o 76 | } 77 | 78 | func polyClipToPolygon(p polyclip.Polygon) Polygon { 79 | pp := make(Polygon, len(p)) 80 | for i, r := range p { 81 | pp[i] = make([]Point, len(r)+1) 82 | for j, ppp := range r { 83 | pp[i][j] = Point(ppp) 84 | } 85 | // Close the ring as per OGC standard. 86 | pp[i][len(r)] = pp[i][0] 87 | } 88 | return pp 89 | } 90 | 91 | // Len returns the number of points in the receiver. 92 | func (p Polygon) Len() int { 93 | var i int 94 | for _, r := range p { 95 | i += len(r) 96 | } 97 | return i 98 | } 99 | 100 | // Points returns an iterator for the points in the receiver. 101 | func (p Polygon) Points() func() Point { 102 | var i, j int 103 | return func() Point { 104 | if i == len(p[j]) { 105 | j++ 106 | i = 0 107 | } 108 | i++ 109 | return p[j][i-1] 110 | } 111 | } 112 | 113 | // Within calculates whether p is within poly. 114 | func (p Polygon) Within(poly Polygonal) WithinStatus { 115 | if reflect.DeepEqual(p, poly) { 116 | return OnEdge 117 | } 118 | for _, r := range p { 119 | for _, pt := range r { 120 | if pointInPolygonal(pt, poly) == Outside { 121 | return Outside 122 | } 123 | } 124 | } 125 | return Inside 126 | } 127 | -------------------------------------------------------------------------------- /encoding/shp/polygon_output_test.go: -------------------------------------------------------------------------------- 1 | package shp 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/ctessum/geom" 10 | ) 11 | 12 | func TestEncoder_polygon(t *testing.T) { 13 | 14 | const testFile = "testdata/test_output" 15 | 16 | type polygon struct { 17 | geom.Polygon 18 | } 19 | 20 | p := polygon{ 21 | Polygon: geom.Polygon{ 22 | geom.Path{ 23 | geom.Point{X: -104, Y: 42}, 24 | geom.Point{X: -104, Y: 44}, 25 | geom.Point{X: -100, Y: 44}, 26 | geom.Point{X: -100, Y: 42}, 27 | geom.Point{X: -104, Y: 42}, 28 | }, 29 | geom.Path{ 30 | geom.Point{X: -103, Y: 45}, 31 | geom.Point{X: -102, Y: 45}, 32 | geom.Point{X: -102, Y: 44}, 33 | geom.Point{X: -103, Y: 44}, 34 | geom.Point{X: -103, Y: 45}, 35 | }, 36 | }, 37 | } 38 | 39 | shape, err := NewEncoder(testFile+".shp", polygon{}) 40 | if err != nil { 41 | t.Fatalf("error creating output shapefile: %v", err) 42 | } 43 | if err = shape.Encode(p); err != nil { 44 | fmt.Printf("error writing output shapefile: %v", err) 45 | } 46 | shape.Close() 47 | 48 | // Load geometries. 49 | d, err := NewDecoder(testFile + ".shp") 50 | if err != nil { 51 | panic(err) 52 | } 53 | 54 | var p2 polygon 55 | d.DecodeRow(&p2) 56 | 57 | if len(p.Polygon) != len(p2.Polygon) { 58 | t.Fatalf("polygons have different numbers of rings: %d != %d", len(p.Polygon), len(p2.Polygon)) 59 | } 60 | if !reflect.DeepEqual(p.Polygon[0], p2.Polygon[0]) { 61 | t.Errorf("ring 0 is different: %+v != %+v", p.Polygon[0], p2.Polygon[0]) 62 | } 63 | if !reflect.DeepEqual(p.Polygon[1], p2.Polygon[1]) { 64 | t.Errorf("ring 1 is different: %+v != %+v", p.Polygon[1], p2.Polygon[1]) 65 | } 66 | 67 | // Check to see if any errors occured during decoding. 68 | if err := d.Error(); err != nil { 69 | t.Fatalf("error decoding shapefile: %v", err) 70 | } 71 | d.Close() 72 | os.Remove(testFile + ".shp") 73 | os.Remove(testFile + ".shx") 74 | os.Remove(testFile + ".dbf") 75 | } 76 | 77 | func TestEncoder_bounds(t *testing.T) { 78 | 79 | const testFile = "testdata/test_output" 80 | 81 | type bounds struct { 82 | *geom.Bounds 83 | } 84 | type polygon struct { 85 | geom.Polygon 86 | } 87 | 88 | p := bounds{ 89 | Bounds: &geom.Bounds{ 90 | Min: geom.Point{0, 0}, 91 | Max: geom.Point{1, 1}, 92 | }, 93 | } 94 | 95 | shape, err := NewEncoder(testFile+".shp", bounds{}) 96 | if err != nil { 97 | t.Fatalf("error creating output shapefile: %v", err) 98 | } 99 | if err = shape.Encode(p); err != nil { 100 | fmt.Printf("error writing output shapefile: %v", err) 101 | } 102 | shape.Close() 103 | 104 | // Load geometries. 105 | d, err := NewDecoder(testFile + ".shp") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | var p2 polygon 111 | d.DecodeRow(&p2) 112 | 113 | // Check to see if any errors occured during decoding. 114 | if err := d.Error(); err != nil { 115 | t.Fatalf("error decoding shapefile: %v", err) 116 | } 117 | d.Close() 118 | 119 | want := geom.Polygon{{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}, {X: 0, Y: 0}}} 120 | 121 | if !reflect.DeepEqual(p2.Polygon, want) { 122 | t.Errorf("%v != %v", p2.Polygon, want) 123 | } 124 | 125 | os.Remove(testFile + ".shp") 126 | os.Remove(testFile + ".shx") 127 | os.Remove(testFile + ".dbf") 128 | 129 | } 130 | -------------------------------------------------------------------------------- /proj/aea.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // AEA is an Albers Conical Equal Area projection. 9 | func AEA(this *SR) (forward, inverse Transformer, err error) { 10 | 11 | if math.Abs(this.Lat1+this.Lat2) < epsln { 12 | err = fmt.Errorf("proj.AEA: standard Parallels cannot be equal and on opposite sides of the equator") 13 | } 14 | temp := this.B / this.A 15 | es := 1 - math.Pow(temp, 2) 16 | e3 := math.Sqrt(es) 17 | 18 | sin_po := math.Sin(this.Lat1) 19 | cos_po := math.Cos(this.Lat1) 20 | //t1 := sin_po 21 | con := sin_po 22 | ms1 := msfnz(e3, sin_po, cos_po) 23 | qs1 := qsfnz(e3, sin_po) 24 | 25 | sin_po = math.Sin(this.Lat2) 26 | cos_po = math.Cos(this.Lat2) 27 | //t2 := sin_po 28 | ms2 := msfnz(e3, sin_po, cos_po) 29 | qs2 := qsfnz(e3, sin_po) 30 | 31 | sin_po = math.Sin(this.Lat0) 32 | //cos_po = math.Cos(this.Lat0) 33 | //t3 := sin_po 34 | qs0 := qsfnz(e3, sin_po) 35 | 36 | var ns0 float64 37 | if math.Abs(this.Lat1-this.Lat2) > epsln { 38 | ns0 = (ms1*ms1 - ms2*ms2) / (qs2 - qs1) 39 | } else { 40 | ns0 = con 41 | } 42 | c := ms1*ms1 + ns0*qs1 43 | rh := this.A * math.Sqrt(c-ns0*qs0) / ns0 44 | 45 | /* Albers Conical Equal Area forward equations--mapping lat,long to x,y 46 | -------------------------------------------------------------------*/ 47 | forward = func(lon, lat float64) (x, y float64, err error) { 48 | 49 | sin_phi := math.Sin(lat) 50 | //cos_phi := math.Cos(lat) 51 | 52 | var qs = qsfnz(e3, sin_phi) 53 | var rh1 = this.A * math.Sqrt(c-ns0*qs) / ns0 54 | var theta = ns0 * adjust_lon(lon-this.Long0) 55 | x = rh1*math.Sin(theta) + this.X0 56 | y = rh - rh1*math.Cos(theta) + this.Y0 57 | return 58 | } 59 | 60 | inverse = func(x, y float64) (lon, lat float64, err error) { 61 | var rh1, qs, con, theta float64 62 | 63 | x -= this.X0 64 | y = rh - y + this.Y0 65 | if ns0 >= 0 { 66 | rh1 = math.Sqrt(x*x + y*y) 67 | con = 1 68 | } else { 69 | rh1 = -math.Sqrt(x*x + y*y) 70 | con = -1 71 | } 72 | theta = 0 73 | if rh1 != 0 { 74 | theta = math.Atan2(con*x, con*y) 75 | } 76 | con = rh1 * ns0 / this.A 77 | if this.sphere { 78 | lat = math.Asin((c - con*con) / (2 * ns0)) 79 | } else { 80 | qs = (c - con*con) / ns0 81 | lat, err = aeaPhi1z(e3, qs) 82 | if err != nil { 83 | return 84 | } 85 | } 86 | 87 | lon = adjust_lon(theta/ns0 + this.Long0) 88 | return 89 | } 90 | return 91 | } 92 | 93 | // aeaPhi1z is a function to compute phi1, the latitude for the inverse of the 94 | // Albers Conical Equal-Area projection. 95 | func aeaPhi1z(eccent, qs float64) (float64, error) { 96 | var sinphi, cosphi, con, com, dphi float64 97 | var phi = asinz(0.5 * qs) 98 | if eccent < epsln { 99 | return phi, nil 100 | } 101 | 102 | var eccnts = eccent * eccent 103 | for i := 1; i <= 25; i++ { 104 | sinphi = math.Sin(phi) 105 | cosphi = math.Cos(phi) 106 | con = eccent * sinphi 107 | com = 1 - con*con 108 | dphi = 0.5 * com * com / cosphi * (qs/(1-eccnts) - sinphi/com + 0.5/eccent*math.Log((1-con)/(1+con))) 109 | phi = phi + dphi 110 | if math.Abs(dphi) <= 1e-7 { 111 | return phi, nil 112 | } 113 | } 114 | return math.NaN(), fmt.Errorf("proj.aeaPhi1z: didn't converge") 115 | } 116 | 117 | func init() { 118 | registerTrans(AEA, "Albers_Conic_Equal_Area", "Albers", "aea") 119 | } 120 | -------------------------------------------------------------------------------- /proj/krovak.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // Krovak is a Krovak projection. 9 | func Krovak(this *SR) (forward, inverse Transformer, err error) { 10 | this.A = 6377397.155 11 | this.Es = 0.006674372230614 12 | this.E = math.Sqrt(this.Es) 13 | if math.IsNaN(this.Lat0) { 14 | this.Lat0 = 0.863937979737193 15 | } 16 | if math.IsNaN(this.Long0) { 17 | this.Long0 = 0.7417649320975901 - 0.308341501185665 18 | } 19 | /* if scale not set default to 0.9999 */ 20 | if math.IsNaN(this.K0) { 21 | this.K0 = 0.9999 22 | } 23 | const S45 = 0.785398163397448 /* 45 */ 24 | const S90 = 2 * S45 25 | Fi0 := this.Lat0 26 | E2 := this.Es 27 | this.E = math.Sqrt(E2) 28 | Alfa := math.Sqrt(1 + (E2*math.Pow(math.Cos(Fi0), 4))/(1-E2)) 29 | const Uq = 1.04216856380474 30 | U0 := math.Asin(math.Sin(Fi0) / Alfa) 31 | G := math.Pow((1+this.E*math.Sin(Fi0))/(1-this.E*math.Sin(Fi0)), Alfa*this.E/2) 32 | K := math.Tan(U0/2+S45) / math.Pow(math.Tan(Fi0/2+S45), Alfa) * G 33 | K1 := this.K0 34 | N0 := this.A * math.Sqrt(1-E2) / (1 - E2*math.Pow(math.Sin(Fi0), 2)) 35 | const S0 = 1.37008346281555 36 | N := math.Sin(S0) 37 | Ro0 := K1 * N0 / math.Tan(S0) 38 | Ad := S90 - Uq 39 | 40 | /* ellipsoid */ 41 | /* calculate xy from lat/lon */ 42 | /* Constants, identical to inverse transform function */ 43 | forward = func(lon, lat float64) (x, y float64, err error) { 44 | var gfi, u, deltav, s, d, eps, ro float64 45 | delta_lon := adjust_lon(lon - this.Long0) 46 | /* Transformation */ 47 | gfi = math.Pow(((1 + this.E*math.Sin(lat)) / (1 - this.E*math.Sin(lat))), (Alfa * this.E / 2)) 48 | u = 2 * (math.Atan(K*math.Pow(math.Tan(lat/2+S45), Alfa)/gfi) - S45) 49 | deltav = -delta_lon * Alfa 50 | s = math.Asin(math.Cos(Ad)*math.Sin(u) + math.Sin(Ad)*math.Cos(u)*math.Cos(deltav)) 51 | d = math.Asin(math.Cos(u) * math.Sin(deltav) / math.Cos(s)) 52 | eps = N * d 53 | ro = Ro0 * math.Pow(math.Tan(S0/2+S45), N) / math.Pow(math.Tan(s/2+S45), N) 54 | y = ro * math.Cos(eps) / 1 55 | x = ro * math.Sin(eps) / 1 56 | 57 | if !this.Czech { 58 | y *= -1 59 | x *= -1 60 | } 61 | return 62 | } 63 | 64 | /* calculate lat/lon from xy */ 65 | inverse = func(x, y float64) (lon, lat float64, err error) { 66 | var u, deltav, s, d, eps, ro, fi1 float64 67 | var ok int 68 | 69 | /* Transformation */ 70 | /* revert y, x*/ 71 | x, y = y, x 72 | if !this.Czech { 73 | y *= -1 74 | x *= -1 75 | } 76 | ro = math.Sqrt(x*x + y*y) 77 | eps = math.Atan2(y, x) 78 | d = eps / math.Sin(S0) 79 | s = 2 * (math.Atan(math.Pow(Ro0/ro, 1/N)*math.Tan(S0/2+S45)) - S45) 80 | u = math.Asin(math.Cos(Ad)*math.Sin(s) - math.Sin(Ad)*math.Cos(s)*math.Cos(d)) 81 | deltav = math.Asin(math.Cos(s) * math.Sin(d) / math.Cos(u)) 82 | x = this.Long0 - deltav/Alfa 83 | fi1 = u 84 | ok = 0 85 | var iter = 0 86 | for { 87 | if !(ok == 0 && iter < 15) { 88 | break 89 | } 90 | y = 2 * (math.Atan(math.Pow(K, -1/Alfa)*math.Pow(math.Tan(u/2+S45), 1/Alfa)*math.Pow((1+this.E*math.Sin(fi1))/(1-this.E*math.Sin(fi1)), this.E/2)) - S45) 91 | if math.Abs(fi1-y) < 0.0000000001 { 92 | ok = 1 93 | } 94 | fi1 = y 95 | iter++ 96 | } 97 | if iter >= 15 { 98 | err = fmt.Errorf("proj.Krovak: iter >= 15") 99 | return 100 | } 101 | 102 | return 103 | } 104 | return 105 | } 106 | 107 | func init() { 108 | registerTrans(Krovak, "Krovak", "krovak") 109 | } 110 | -------------------------------------------------------------------------------- /transform.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "github.com/ctessum/geom/proj" 5 | ) 6 | 7 | // Transform shifts the coordinates of p according to t. 8 | func (p Point) Transform(t proj.Transformer) (Geom, error) { 9 | if t == nil { 10 | return p, nil 11 | } 12 | var err error 13 | p2 := Point{} 14 | p2.X, p2.Y, err = t(p.X, p.Y) 15 | return p2, err 16 | } 17 | 18 | // Transform shifts the coordinates of mp according to t. 19 | func (mp MultiPoint) Transform(t proj.Transformer) (Geom, error) { 20 | if t == nil { 21 | return mp, nil 22 | } 23 | mp2 := make(MultiPoint, len(mp)) 24 | for i, p := range mp { 25 | g, err := p.Transform(t) 26 | if err != nil { 27 | return nil, err 28 | } 29 | mp2[i] = g.(Point) 30 | } 31 | return mp2, nil 32 | } 33 | 34 | // Transform shifts the coordinates of l according to t. 35 | func (l LineString) Transform(t proj.Transformer) (Geom, error) { 36 | if t == nil { 37 | return l, nil 38 | } 39 | l2 := make(LineString, len(l)) 40 | var err error 41 | for i, p := range l { 42 | p2 := Point{} 43 | p2.X, p2.Y, err = t(p.X, p.Y) 44 | if err != nil { 45 | return nil, err 46 | } 47 | l2[i] = p2 48 | } 49 | return l2, nil 50 | } 51 | 52 | // Transform shifts the coordinates of ml according to t. 53 | func (ml MultiLineString) Transform(t proj.Transformer) (Geom, error) { 54 | if t == nil { 55 | return ml, nil 56 | } 57 | ml2 := make(MultiLineString, len(ml)) 58 | for i, l := range ml { 59 | g, err := l.Transform(t) 60 | ml2[i] = g.(LineString) 61 | if err != nil { 62 | return nil, err 63 | } 64 | } 65 | return ml2, nil 66 | } 67 | 68 | // Transform shifts the coordinates of p according to t. 69 | func (p Polygon) Transform(t proj.Transformer) (Geom, error) { 70 | if t == nil { 71 | return p, nil 72 | } 73 | p2 := make(Polygon, len(p)) 74 | var err error 75 | for i, r := range p { 76 | p2[i] = make([]Point, len(r)) 77 | for j, pp := range r { 78 | pp2 := Point{} 79 | pp2.X, pp2.Y, err = t(pp.X, pp.Y) 80 | if err != nil { 81 | return nil, err 82 | } 83 | p2[i][j] = pp2 84 | } 85 | } 86 | return p2, nil 87 | } 88 | 89 | // Transform shifts the coordinates of mp according to t. 90 | func (mp MultiPolygon) Transform(t proj.Transformer) (Geom, error) { 91 | if t == nil { 92 | return mp, nil 93 | } 94 | mp2 := make(MultiPolygon, len(mp)) 95 | for i, p := range mp { 96 | g, err := p.Transform(t) 97 | mp2[i] = g.(Polygon) 98 | if err != nil { 99 | return nil, err 100 | } 101 | } 102 | return mp2, nil 103 | } 104 | 105 | // Transform shifts the coordinates of gc according to t. 106 | func (gc GeometryCollection) Transform(t proj.Transformer) (Geom, error) { 107 | if t == nil { 108 | return gc, nil 109 | } 110 | gc2 := make(GeometryCollection, len(gc)) 111 | var err error 112 | for i, g := range gc { 113 | gc2[i], err = g.Transform(t) 114 | if err != nil { 115 | return nil, err 116 | } 117 | } 118 | return gc2, nil 119 | } 120 | 121 | // Transform shifts the coordinates of b according to t. 122 | // If t is not nil, this function returns a Polygon instead of a *Bounds 123 | // because the transformed polygon may not match the transformed bounding 124 | // rectangle. 125 | func (b *Bounds) Transform(t proj.Transformer) (Geom, error) { 126 | if t == nil { 127 | return b, nil 128 | } 129 | p := Polygon{{b.Min, {X: b.Max.X, Y: b.Min.Y}, b.Max, {X: b.Min.X, Y: b.Max.Y}}} 130 | return p.Transform(t) 131 | } 132 | -------------------------------------------------------------------------------- /proj/datum_transform.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | const ( 9 | srsWGS84SemiMajor = 6378137 // only used in grid shift transforms 10 | srsWGS84ESquared = 0.006694379990141316 //DGR: 2012-07-29 11 | ) 12 | 13 | func checkDatumParams(fallback datumType) bool { 14 | return (fallback == pjd3Param || fallback == pjd7Param) 15 | } 16 | 17 | func datumTransform(source, dest *datum, x, y, z float64) (float64, float64, float64, error) { 18 | var err error 19 | 20 | // Short cut if the datums are identical. 21 | if source.compare_datums(dest) { 22 | return x, y, z, nil // in this case, zero is sucess, 23 | // whereas cs_compare_datums returns 1 to indicate TRUE 24 | // confusing, should fix this 25 | } 26 | 27 | // Explicitly skip datum transform by setting 'datum=none' as parameter for either source or dest 28 | if source.datum_type == pjdNoDatum || dest.datum_type == pjdNoDatum { 29 | return x, y, z, nil 30 | } 31 | 32 | //DGR: 2012-07-29 : add nadgrids support (begin) 33 | var src_a = source.a 34 | var src_es = source.es 35 | 36 | var dst_a = dest.a 37 | var dst_es = dest.es 38 | 39 | var fallback = source.datum_type 40 | // If this datum requires grid shifts, then apply it to geodetic coordinates. 41 | if fallback == pjdGridShift { 42 | err := fmt.Errorf("in proj.datumTransform: gridshift not supported") 43 | return math.NaN(), math.NaN(), math.NaN(), err 44 | /*if this.apply_gridshift(source, 0, x, y, z) == 0 { 45 | source.a = SRS_WGS84_SEMIMAJOR 46 | source.es = SRS_WGS84_ESQUARED 47 | } else { 48 | // try 3 or 7 params transformation or nothing ? 49 | if len(source.datum_params) == 0 { 50 | source.a = src_a 51 | source.es = source.es 52 | return x, y, z, nil 53 | } 54 | wp = 1 55 | for i := 0; i < len(source.datum_params); i++ { 56 | wp *= source.datum_params[i] 57 | } 58 | if wp == 0 { 59 | source.a = src_a 60 | source.es = source.es 61 | return x, y, z, nil 62 | } 63 | if len(source.datum_params) > 3 { 64 | fallback = PJD_7PARAM 65 | } else { 66 | fallback = PJD_3PARAM 67 | } 68 | }*/ 69 | } 70 | if dest.datum_type == pjdGridShift { 71 | dest.a = srsWGS84SemiMajor 72 | dest.es = srsWGS84ESquared 73 | } 74 | // Do we need to go through geocentric coordinates? 75 | if source.es != dest.es || source.a != dest.a || checkDatumParams(fallback) || 76 | checkDatumParams(dest.datum_type) { 77 | //DGR: 2012-07-29 : add nadgrids support (end) 78 | // Convert to geocentric coordinates. 79 | x, y, z, err = source.geodetic_to_geocentric(x, y, z) 80 | if err != nil { 81 | return math.NaN(), math.NaN(), math.NaN(), err 82 | } 83 | // CHECK_RETURN; 84 | // Convert between datums 85 | if checkDatumParams(source.datum_type) { 86 | x, y, z = source.geocentric_to_wgs84(x, y, z) 87 | // CHECK_RETURN; 88 | } 89 | if checkDatumParams(dest.datum_type) { 90 | x, y, z = dest.geocentric_from_wgs84(x, y, z) 91 | // CHECK_RETURN; 92 | } 93 | // Convert back to geodetic coordinates 94 | x, y, z = dest.geocentric_to_geodetic(x, y, z) 95 | // CHECK_RETURN; 96 | } 97 | // Apply grid shift to destination if required 98 | if dest.datum_type == pjdGridShift { 99 | err := fmt.Errorf("in proj.datumTransform: gridshift not supported") 100 | return math.NaN(), math.NaN(), math.NaN(), err 101 | //this.apply_gridshift(dest, 1, x, y, z) 102 | // CHECK_RETURN; 103 | } 104 | 105 | source.a = src_a 106 | source.es = src_es 107 | dest.a = dst_a 108 | dest.es = dst_es 109 | 110 | return x, y, z, nil 111 | } 112 | -------------------------------------------------------------------------------- /multipolygon.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/ctessum/polyclip-go" 7 | ) 8 | 9 | // MultiPolygon is a holder for multiple related polygons. 10 | type MultiPolygon []Polygon 11 | 12 | // Bounds gives the rectangular extents of the MultiPolygon. 13 | func (mp MultiPolygon) Bounds() *Bounds { 14 | b := NewBounds() 15 | for _, polygon := range mp { 16 | b.Extend(polygon.Bounds()) 17 | } 18 | return b 19 | } 20 | 21 | // Area returns the combined area of the polygons in p. 22 | // The function works correctly for polygons with 23 | // holes, regardless of the winding order of the holes, but may give the wrong 24 | // result for self-intersecting polygons, or polygons in mp that overlap each other. 25 | func (mp MultiPolygon) Area() float64 { 26 | a := 0. 27 | for _, pp := range mp { 28 | a += pp.Area() 29 | } 30 | return math.Abs(a) 31 | } 32 | 33 | // Intersection returns the area(s) shared by mp and p2. 34 | func (mp MultiPolygon) Intersection(p2 Polygonal) Polygonal { 35 | return mp.op(p2, polyclip.INTERSECTION) 36 | } 37 | 38 | // Union returns the combination of mp and p2. 39 | func (mp MultiPolygon) Union(p2 Polygonal) Polygonal { 40 | return mp.op(p2, polyclip.UNION) 41 | } 42 | 43 | // XOr returns the area(s) occupied by either mp or p2 but not both. 44 | func (mp MultiPolygon) XOr(p2 Polygonal) Polygonal { 45 | return mp.op(p2, polyclip.XOR) 46 | } 47 | 48 | // Difference subtracts p2 from mp. 49 | func (mp MultiPolygon) Difference(p2 Polygonal) Polygonal { 50 | return mp.op(p2, polyclip.DIFFERENCE) 51 | } 52 | 53 | func (mp MultiPolygon) op(p2 Polygonal, op polyclip.Op) Polygonal { 54 | var pp polyclip.Polygon 55 | for _, ppx := range mp { 56 | pp = append(pp, ppx.toPolyClip()...) 57 | } 58 | var pp2 polyclip.Polygon 59 | for _, pp2x := range p2.Polygons() { 60 | pp2 = append(pp2, pp2x.toPolyClip()...) 61 | } 62 | return polyClipToPolygon(pp.Construct(op, pp2)) 63 | } 64 | 65 | // Polygons returns the polygons that make up mp. 66 | func (mp MultiPolygon) Polygons() []Polygon { 67 | return mp 68 | } 69 | 70 | // Centroid calculates the centroid of mp, from 71 | // wikipedia: http://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon. 72 | // The polygon can have holes, but each ring must be closed (i.e., 73 | // p[0] == p[n-1], where the ring has n points) and must not be 74 | // self-intersecting. 75 | // The algorithm will not check to make sure the holes are 76 | // actually inside the outer rings. 77 | func (mp MultiPolygon) Centroid() Point { 78 | var A, xA, yA float64 79 | for _, p := range mp { 80 | b := p.ringBounds() 81 | for i, r := range p { 82 | a := area(r, i, p, b) 83 | cx, cy := 0., 0. 84 | for i := 0; i < len(r)-1; i++ { 85 | cx += (r[i].X + r[i+1].X) * 86 | (r[i].X*r[i+1].Y - r[i+1].X*r[i].Y) 87 | cy += (r[i].Y + r[i+1].Y) * 88 | (r[i].X*r[i+1].Y - r[i+1].X*r[i].Y) 89 | } 90 | cx /= 6 * a 91 | cy /= 6 * a 92 | A += a 93 | xA += cx * a 94 | yA += cy * a 95 | } 96 | } 97 | return Point{X: xA / A, Y: yA / A} 98 | } 99 | 100 | // Len returns the number of points in the receiver. 101 | func (mp MultiPolygon) Len() int { 102 | var i int 103 | for _, p := range mp { 104 | i += p.Len() 105 | } 106 | return i 107 | } 108 | 109 | // Points returns an iterator for the points in the receiver. 110 | func (mp MultiPolygon) Points() func() Point { 111 | var i, j, k int 112 | return func() Point { 113 | if i == len(mp[k][j]) { 114 | j++ 115 | i = 0 116 | if j == len(mp[k]) { 117 | k++ 118 | j = 0 119 | } 120 | } 121 | i++ 122 | return mp[k][j][i-1] 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /proj/transform.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | func checkNotWGS(source, dest *SR) bool { 9 | return ((source.datum.datum_type == pjd3Param || source.datum.datum_type == pjd7Param) && dest.DatumCode != "WGS84") 10 | } 11 | 12 | const enu = "enu" 13 | const longlat = "longlat" 14 | 15 | // NewTransform creates a function that transforms a point from sr 16 | // to the destination spatial reference. If source ~= dest, the returned 17 | // Transformer will be nil. 18 | func (source *SR) NewTransform(dest *SR) (Transformer, error) { 19 | if dest == nil { 20 | return nil, fmt.Errorf("proj: destination is nil") 21 | } 22 | 23 | // If source and dest are the same, we don't need to do any transforming 24 | const ulpTolerance = 3 // Our tolerance is 3 units in the last place 25 | if source.Equal(dest, 3) { 26 | return nil, nil 27 | } 28 | 29 | return func(x, y float64) (float64, float64, error) { 30 | point := []float64{x, y} 31 | // Workaround for datum shifts towgs84, if either source or destination projection is not wgs84 32 | if checkNotWGS(source, dest) || checkNotWGS(dest, source) { 33 | wgs84, err := Parse("WGS84") 34 | if err != nil { 35 | return math.NaN(), math.NaN(), err 36 | } 37 | t, err := source.NewTransform(wgs84) 38 | if err != nil { 39 | return math.NaN(), math.NaN(), err 40 | } 41 | point[0], point[1], err = t(point[0], point[1]) 42 | if err != nil { 43 | return math.NaN(), math.NaN(), err 44 | } 45 | source = wgs84 46 | } 47 | _, sourceInverse, err := source.Transformers() 48 | if err != nil { 49 | return math.NaN(), math.NaN(), err 50 | } 51 | destForward, _, err := dest.Transformers() 52 | if err != nil { 53 | return math.NaN(), math.NaN(), err 54 | } 55 | 56 | // DGR, 2010/11/12 57 | if source.Axis != enu { 58 | point, err = adjust_axis(source, false, point) 59 | if err != nil { 60 | return math.NaN(), math.NaN(), err 61 | } 62 | } 63 | // Transform source points to long/lat, if they aren't already. 64 | if source.Name == longlat { 65 | point[0] *= deg2rad // convert degrees to radians 66 | point[1] *= deg2rad 67 | } else { 68 | point[0] *= source.ToMeter 69 | point[1] *= source.ToMeter 70 | point[0], point[1], err = sourceInverse(point[0], point[1]) // Convert Cartesian to longlat 71 | if err != nil { 72 | return math.NaN(), math.NaN(), err 73 | } 74 | } 75 | // Adjust for the prime meridian if necessary 76 | if !math.IsNaN(source.FromGreenwich) { 77 | point[0] += source.FromGreenwich 78 | } 79 | 80 | // Convert datums if needed, and if possible. 81 | z := 0. 82 | if len(point) == 3 { 83 | z = point[2] 84 | } 85 | point[0], point[1], z, err = datumTransform(source.datum, dest.datum, 86 | point[0], point[1], z) 87 | if err != nil { 88 | return math.NaN(), math.NaN(), err 89 | } 90 | if len(point) == 3 { 91 | point[2] = z 92 | } 93 | 94 | // Adjust for the prime meridian if necessary 95 | if !math.IsNaN(dest.FromGreenwich) { 96 | point[0] -= dest.FromGreenwich 97 | } 98 | 99 | if dest.Name == longlat { 100 | // convert radians to decimal degrees 101 | point[0] *= r2d 102 | point[1] *= r2d 103 | } else { // else project 104 | point[0], point[1], err = destForward(point[0], point[1]) 105 | if err != nil { 106 | return math.NaN(), math.NaN(), err 107 | } 108 | point[0] /= dest.ToMeter 109 | point[1] /= dest.ToMeter 110 | } 111 | 112 | // DGR, 2010/11/12 113 | if dest.Axis != enu { 114 | point, err = adjust_axis(dest, true, point) 115 | if err != nil { 116 | return math.NaN(), math.NaN(), err 117 | } 118 | } 119 | 120 | return point[0], point[1], nil 121 | }, nil 122 | } 123 | -------------------------------------------------------------------------------- /simplify_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSimplify(t *testing.T) { 10 | type simplifyTest struct { 11 | input LineString 12 | output LineString 13 | tolerance float64 14 | } 15 | 16 | var line = simplifyTest{ 17 | input: LineString{ 18 | Point{153.52, 928.49}, 19 | Point{240.79, 988.95}, 20 | Point{323.34, 1014.40}, 21 | Point{404.41, 1020.08}, 22 | Point{475.60, 981.17}, 23 | Point{497.37, 921.45}, 24 | Point{546.26, 903.57}, 25 | Point{598.10, 907.57}, 26 | Point{655.31, 941.11}, 27 | Point{679.28, 1004.20}, 28 | Point{630.91, 1052.36}, 29 | Point{581.17, 1029.23}, 30 | }, 31 | output: LineString{ 32 | Point{153.52, 928.49}, 33 | Point{404.41, 1020.08}, 34 | Point{546.26, 903.57}, 35 | Point{655.31, 941.11}, 36 | Point{679.28, 1004.20}, 37 | Point{630.91, 1052.36}, 38 | Point{581.17, 1029.23}, 39 | }, 40 | tolerance: 30.0, 41 | } 42 | var spiral = simplifyTest{ 43 | input: LineString{ 44 | Point{70.57, 609.01}, 45 | Point{102.21, 618.89}, 46 | Point{125.19, 635.79}, 47 | Point{133.07, 659.34}, 48 | Point{134.86, 688.40}, 49 | Point{121.04, 709.80}, 50 | Point{104.15, 726.70}, 51 | Point{80.45, 731.71}, 52 | Point{56.40, 729.34}, 53 | Point{37.86, 714.81}, 54 | Point{22.83, 692.69}, 55 | Point{23.19, 669.21}, 56 | Point{33.42, 648.38}, 57 | Point{49.74, 635.79}, 58 | Point{84.03, 628.63}, 59 | Point{115.31, 645.31}, 60 | Point{118.75, 681.96}, 61 | Point{109.94, 704.43}, 62 | Point{84.39, 715.17}, 63 | Point{60.20, 716.24}, 64 | Point{42.37, 703.14}, 65 | Point{34.64, 675.16}, 66 | Point{46.31, 658.05}, 67 | Point{69.50, 645.16}, 68 | Point{85.68, 651.96}, 69 | Point{98.78, 669.93}, 70 | Point{92.84, 691.98}, 71 | Point{68.07, 699.21}, 72 | Point{72.58, 676.59}, 73 | }, 74 | output: LineString{ 75 | Point{70.57, 609.01}, 76 | Point{133.07, 659.34}, 77 | Point{104.15, 726.70}, 78 | Point{37.86, 714.81}, 79 | Point{23.19, 669.21}, 80 | Point{84.03, 628.63}, 81 | Point{118.75, 681.96}, 82 | Point{60.20, 716.24}, 83 | Point{46.31, 658.05}, 84 | Point{98.78, 669.93}, 85 | Point{72.58, 676.59}, 86 | }, 87 | tolerance: 30.0, 88 | } 89 | 90 | for _, test := range []simplifyTest{line, spiral} { 91 | o := test.input.Simplify(test.tolerance) 92 | if !reflect.DeepEqual(o, test.output) { 93 | t.Errorf("%v should equal %v.", o, test.output) 94 | t.Logf("%v should equal %v.", o, test.output) 95 | } 96 | } 97 | } 98 | 99 | func TestSimplifyInfiniteLoop(t *testing.T) { 100 | // This is a self-intersecting shape. 101 | geometry := Polygon{[]Point{ 102 | Point{X: -871773.1638742175, Y: 497165.8489278648}, 103 | Point{X: -871974.6604566738, Y: 496416.7107209433}, 104 | Point{X: -871878.9516291074, Y: 497176.64415429346}, 105 | Point{X: -871773.1638742175, Y: 497165.8489278648}, 106 | Point{X: -999958.0564939477, Y: 242680.11889008153}}} 107 | 108 | ch := make(chan int) 109 | go func() { 110 | geometry.Simplify(100.) 111 | ch <- 0 112 | }() 113 | 114 | select { 115 | case <-ch: 116 | case <-time.After(1 * time.Second): 117 | t.Errorf("Simplify %+v timed out.", geometry) 118 | } 119 | } 120 | 121 | // This test creates a degenerate polygon. It's not ideal but 122 | // in this case it's what the algorithm is designed to do. 123 | func TestSimplifyDegenerate(t *testing.T) { 124 | geometry := Polygon{[]Point{ 125 | Point{X: 1, Y: 1}, 126 | Point{X: 1, Y: 3}, 127 | Point{X: 1.25, Y: 2}, 128 | Point{X: 1, Y: 1}, 129 | }} 130 | got := geometry.Simplify(100.) 131 | 132 | want := Polygon{[]Point{ 133 | Point{X: 1, Y: 1}, 134 | Point{X: 1, Y: 1}, 135 | }} 136 | 137 | if !reflect.DeepEqual(got, want) { 138 | t.Errorf("want %#v, got %#v", want, got) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /test/generate-test-data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import random 4 | import sys 5 | 6 | import shapely.geometry 7 | 8 | 9 | R = random.Random(0) 10 | 11 | 12 | def r(): 13 | return float(R.randint(-1000000, 1000000)) / 1000000 14 | 15 | 16 | def randomCoord(): 17 | return (r(), r()) 18 | 19 | 20 | def randomCoords(n): 21 | return [(r(), r()) for i in xrange(n)] 22 | 23 | 24 | class RandomPoint(shapely.geometry.Point): 25 | 26 | def __init__(self, coord=None): 27 | if coord is None: 28 | coord = randomCoord() 29 | shapely.geometry.Point.__init__(self, coord) 30 | 31 | def goify(self): 32 | return 'geom.Point{%f, %f}' % (self.x, self.y) 33 | 34 | 35 | class RandomLineString(shapely.geometry.LineString): 36 | 37 | def __init__(self, coords=None): 38 | if coords is None: 39 | coords = randomCoords(R.randint(2, 8)) 40 | shapely.geometry.LineString.__init__(self, coords) 41 | 42 | def goify(self): 43 | return 'geom.LineString{[]geom.Point{' + ', '.join('{%f, %f}' % c for c in self.coords) + '}}' 44 | 45 | 46 | class RandomPolygon(shapely.geometry.Polygon): 47 | 48 | def __init__(self, rings=None): 49 | if rings is None: 50 | rings = [randomCoords(R.randint(3, 8))] + [randomCoords(R.randint(3, 8)) for i in xrange(R.randint(0, 4))] 51 | shapely.geometry.Polygon.__init__(self, rings[0], rings[1:]) 52 | 53 | def goify(self): 54 | rings = [self.exterior.coords] + [i.coords for i in self.interiors] 55 | return 'geom.Polygon{[]geom.Path{' + ', '.join('{' + ', '.join('{%f, %f}' % c for c in ring) + '}' for ring in rings) + '}}' 56 | 57 | 58 | class RandomMultiPoint(shapely.geometry.MultiPoint): 59 | 60 | def __init__(self): 61 | shapely.geometry.MultiPoint.__init__(self, [RandomPoint() for i in xrange(R.randint(1, 8))]) 62 | 63 | def goify(self): 64 | return 'geom.MultiPoint{[]geom.Point{' + ', '.join(RandomPoint(g.coords[0]).goify() for g in self.geoms) + '}}' 65 | 66 | 67 | class RandomMultiLineString(shapely.geometry.MultiLineString): 68 | 69 | def __init__(self): 70 | shapely.geometry.MultiLineString.__init__(self, [RandomLineString() for i in xrange(R.randint(1, 8))]) 71 | 72 | def goify(self): 73 | return 'geom.MultiLineString{[]geom.LineString{' + ', '.join(RandomLineString(g.coords).goify() for g in self.geoms) + '}}' 74 | 75 | 76 | class RandomMultiPolygon(shapely.geometry.MultiPolygon): 77 | 78 | def __init__(self): 79 | shapely.geometry.MultiPolygon.__init__(self, [RandomPolygon() for i in xrange(R.randint(1, 8))]) 80 | 81 | def goify(self): 82 | return 'geom.MultiPolygon{[]geom.Polygon{' + ', '.join(RandomPolygon([g.exterior] + list(g.interiors)).goify() for g in self.geoms) + '}}' 83 | 84 | 85 | def main(argv): 86 | # FIXME add GeoJSON support 87 | print 'package test' 88 | print 89 | print 'import (' 90 | print '\t"github.com/twpayne/gogeom/geom"' 91 | print ')' 92 | print 93 | print 'var cases = []struct {' 94 | print '\tg geom.Geom' 95 | print '\thex string' 96 | print '\twkb []byte' 97 | print '\twkt string' 98 | print '}{' 99 | for klass in ( 100 | RandomPoint, 101 | RandomLineString, 102 | RandomPolygon, 103 | RandomMultiPoint, 104 | RandomMultiLineString, 105 | RandomMultiPolygon): 106 | for i in xrange(8): 107 | g = klass() 108 | print '\t{' 109 | print '\t\t%s,' % (g.goify(),) 110 | print '\t\t"%s",' % (g.wkb.encode('hex'),) 111 | print '\t\t[]byte("%s"),' % (''.join('\\x%02X' % ord(c) for c in g.wkb),) 112 | print '\t\t"%s",' % (g.wkt,) 113 | print '\t},' 114 | print '}' 115 | 116 | 117 | if __name__ == '__main__': 118 | sys.exit(main(sys.argv)) 119 | -------------------------------------------------------------------------------- /proj/tmerc.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // TMerc is a transverse Mercator projection. 9 | func TMerc(this *SR) (forward, inverse Transformer, err error) { 10 | 11 | e0 := e0fn(this.Es) 12 | e1 := e1fn(this.Es) 13 | e2 := e2fn(this.Es) 14 | e3 := e3fn(this.Es) 15 | ml0 := this.A * mlfn(e0, e1, e2, e3, this.Lat0) 16 | 17 | /** 18 | Transverse Mercator Forward - long/lat to x/y 19 | long/lat in radians 20 | */ 21 | forward = func(lon, lat float64) (x, y float64, err error) { 22 | 23 | var delta_lon = adjust_lon(lon - this.Long0) 24 | var con float64 25 | var sin_phi = math.Sin(lat) 26 | var cos_phi = math.Cos(lat) 27 | 28 | if this.sphere { 29 | var b = cos_phi * math.Sin(delta_lon) 30 | if (math.Abs(math.Abs(b) - 1)) < 0.0000000001 { 31 | return math.NaN(), math.NaN(), fmt.Errorf("in proj.TMerc forward: b == 0") 32 | } 33 | x = 0.5 * this.A * this.K0 * math.Log((1+b)/(1-b)) 34 | con = math.Acos(cos_phi * math.Cos(delta_lon) / math.Sqrt(1-b*b)) 35 | if lat < 0 { 36 | con = -con 37 | } 38 | y = this.A * this.K0 * (con - this.Lat0) 39 | 40 | } else { 41 | var al = cos_phi * delta_lon 42 | var als = math.Pow(al, 2) 43 | var c = this.Ep2 * math.Pow(cos_phi, 2) 44 | var tq = math.Tan(lat) 45 | var t = math.Pow(tq, 2) 46 | con = 1 - this.Es*math.Pow(sin_phi, 2) 47 | var n = this.A / math.Sqrt(con) 48 | var ml = this.A * mlfn(e0, e1, e2, e3, lat) 49 | 50 | x = this.K0*n*al*(1+als/6*(1-t+c+als/20*(5-18*t+math.Pow(t, 2)+72*c-58*this.Ep2))) + this.X0 51 | y = this.K0*(ml-ml0+n*tq*(als*(0.5+als/24*(5-t+9*c+4*math.Pow(c, 2)+als/30*(61-58*t+math.Pow(t, 2)+600*c-330*this.Ep2))))) + this.Y0 52 | 53 | } 54 | return 55 | } 56 | 57 | /** 58 | Transverse Mercator Inverse - x/y to long/lat 59 | */ 60 | inverse = func(x, y float64) (lon, lat float64, err error) { 61 | var con, phi float64 62 | var delta_phi float64 63 | const max_iter = 6 64 | 65 | if this.sphere { 66 | var f = math.Exp(x / (this.A * this.K0)) 67 | var g = 0.5 * (f - 1/f) 68 | var temp = this.Lat0 + y/(this.A*this.K0) 69 | var h = math.Cos(temp) 70 | con = math.Sqrt((1 - h*h) / (1 + g*g)) 71 | lat = asinz(con) 72 | if temp < 0 { 73 | lat = -lat 74 | } 75 | if (g == 0) && (h == 0) { 76 | lon = this.Long0 77 | } else { 78 | lon = adjust_lon(math.Atan2(g, h) + this.Long0) 79 | } 80 | } else { // ellipsoidal form 81 | var x = x - this.X0 82 | var y = y - this.Y0 83 | 84 | con = (ml0 + y/this.K0) / this.A 85 | phi = con 86 | i := 0 87 | for { 88 | delta_phi = ((con + e1*math.Sin(2*phi) - e2*math.Sin(4*phi) + e3*math.Sin(6*phi)) / e0) - phi 89 | phi += delta_phi 90 | if math.Abs(delta_phi) <= epsln { 91 | break 92 | } 93 | if i >= max_iter { 94 | return math.NaN(), math.NaN(), fmt.Errorf("in proj.TMerc inverse: i > max_iter") 95 | } 96 | i++ 97 | } 98 | if math.Abs(phi) < halfPi { 99 | var sin_phi = math.Sin(phi) 100 | var cos_phi = math.Cos(phi) 101 | var tan_phi = math.Tan(phi) 102 | var c = this.Ep2 * math.Pow(cos_phi, 2) 103 | var cs = math.Pow(c, 2) 104 | var t = math.Pow(tan_phi, 2) 105 | var ts = math.Pow(t, 2) 106 | con = 1 - this.Es*math.Pow(sin_phi, 2) 107 | var n = this.A / math.Sqrt(con) 108 | var r = n * (1 - this.Es) / con 109 | var d = x / (n * this.K0) 110 | var ds = math.Pow(d, 2) 111 | lat = phi - (n*tan_phi*ds/r)*(0.5-ds/24*(5+3*t+10*c-4*cs-9*this.Ep2-ds/30*(61+90*t+298*c+45*ts-252*this.Ep2-3*cs))) 112 | lon = adjust_lon(this.Long0 + (d * (1 - ds/6*(1+2*t+c-ds/20*(5-2*c+28*t-3*cs+8*this.Ep2+24*ts))) / cos_phi)) 113 | } else { 114 | lat = halfPi * sign(y) 115 | lon = this.Long0 116 | } 117 | } 118 | return 119 | } 120 | return 121 | } 122 | 123 | func init() { 124 | registerTrans(TMerc, "Transverse_Mercator", "Transverse Mercator", "tmerc") 125 | } 126 | -------------------------------------------------------------------------------- /area_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "testing" 4 | 5 | func TestArea(t *testing.T) { 6 | tests := []struct { 7 | test Polygonal 8 | expected float64 9 | }{ 10 | { 11 | test: Polygon{{ 12 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 13 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 14 | Point{X: 0, Y: 0}}}, 15 | expected: 4., 16 | }, 17 | { // backwards square. 18 | test: Polygon{{ 19 | Point{X: 0, Y: 0}, Point{X: 0, Y: 2}, 20 | Point{X: 2, Y: 2}, Point{X: 2, Y: 0}, 21 | Point{X: 0, Y: 0}}}, 22 | expected: 4., 23 | }, 24 | { // Square without closing point 25 | test: Polygon{{ 26 | Point{X: 0, Y: 0}, Point{X: 0, Y: 2}, 27 | Point{X: 2, Y: 2}, Point{X: 2, Y: 0}}}, 28 | expected: 4., 29 | }, 30 | { // Square with hole 31 | test: Polygon{ 32 | { 33 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 34 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 35 | Point{X: 0, Y: 0}, 36 | }, 37 | { 38 | Point{X: 0.5, Y: 0.5}, Point{X: 0.5, Y: 1.5}, 39 | Point{X: 1.5, Y: 1.5}, Point{X: 1.5, Y: 0.5}, 40 | Point{X: 0.5, Y: 0.5}, 41 | }, 42 | }, 43 | expected: 3., 44 | }, 45 | { // Square with backwards hole. 46 | test: Polygon{ 47 | { 48 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 49 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 50 | Point{X: 0, Y: 0}, 51 | }, 52 | { 53 | Point{X: 0.5, Y: 0.5}, Point{X: 1.5, Y: 0.5}, 54 | Point{X: 1.5, Y: 1.5}, Point{X: 0.5, Y: 1.5}, 55 | Point{X: 0.5, Y: 0.5}, 56 | }, 57 | }, 58 | expected: 3., 59 | }, 60 | { 61 | test: MultiPolygon{ 62 | {{ 63 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 64 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 65 | Point{X: 0, Y: 0}, 66 | }}, 67 | {{ 68 | Point{X: 2, Y: 2}, Point{X: 4, Y: 2}, 69 | Point{X: 4, Y: 4}, Point{X: 2, Y: 4}, 70 | Point{X: 2, Y: 2}, 71 | }}, 72 | }, 73 | expected: 8., 74 | }, 75 | { 76 | // Polygon with inner ring that touches edge. 77 | test: Polygon{ 78 | []Point{ 79 | Point{X: 0, Y: 1}, 80 | Point{X: 1, Y: 0}, 81 | Point{X: 2, Y: 1}, 82 | Point{X: 1, Y: 2}, 83 | }, 84 | []Point{ 85 | Point{X: 0, Y: 1}, 86 | Point{X: 1, Y: 0.5}, 87 | Point{X: 2, Y: 1}, 88 | Point{X: 1, Y: 1.5}, 89 | }, 90 | }, 91 | expected: 2. - 1., 92 | }, 93 | { 94 | // Polygon with inner ring where all points of the inner ring 95 | // touch the edge. 96 | test: Polygon{ 97 | []Point{ 98 | Point{X: 0, Y: 0}, 99 | Point{X: 2, Y: 0}, 100 | Point{X: 2, Y: 2}, 101 | Point{X: 0, Y: 2}, 102 | }, 103 | []Point{ 104 | Point{X: 0, Y: 1}, 105 | Point{X: 1, Y: 0}, 106 | Point{X: 2, Y: 1}, 107 | Point{X: 1, Y: 2}, 108 | }, 109 | }, 110 | expected: 4. - 2., 111 | }, 112 | { 113 | // Polygon with inner ring where all points of the inner ring 114 | // touch the edge. 115 | test: Polygon{ 116 | []Point{ 117 | Point{X: 0, Y: 0}, 118 | Point{X: 2, Y: 0}, 119 | Point{X: 2, Y: 2}, 120 | Point{X: 0, Y: 2}, 121 | }, 122 | []Point{ 123 | Point{X: 1, Y: 0}, 124 | Point{X: 2, Y: 2}, 125 | Point{X: 0, Y: 2}, 126 | }, 127 | }, 128 | expected: 4. - 2., 129 | }, 130 | { 131 | // Polygon with outer ring where some of points of the inner ring 132 | // touch the edge. 133 | test: Polygon{ 134 | []Point{ 135 | Point{X: 0, Y: 0}, 136 | Point{X: 2, Y: 0}, 137 | Point{X: 2, Y: 2}, 138 | Point{X: 0, Y: 2}, 139 | }, 140 | []Point{ 141 | Point{X: 0, Y: 0}, 142 | Point{X: 0, Y: 1}, 143 | Point{X: 0, Y: 2}, 144 | Point{X: -1, Y: 1}, 145 | }, 146 | }, 147 | expected: 4. + 1., 148 | }, 149 | } 150 | for i, test := range tests { 151 | result := test.test.Area() 152 | if result != test.expected { 153 | t.Errorf("%d: expected %g, got %g", i, test.expected, result) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /proj/projString.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | ) 8 | 9 | const deg2rad = 0.01745329251994329577 10 | 11 | func projString(defData string) (*SR, error) { 12 | self := NewSR() 13 | var err error 14 | for i, a := range strings.Split(defData, "+") { 15 | if i == 0 { 16 | continue // skip everything to the left of the first + 17 | } 18 | a = strings.TrimSpace(a) 19 | split := strings.Split(a, "=") 20 | split = append(split, "true") 21 | paramName := strings.ToLower(split[0]) 22 | paramVal := split[1] 23 | 24 | switch paramName { 25 | case "proj": 26 | self.Name = paramVal 27 | case "title": 28 | self.Title = paramVal 29 | case "datum": 30 | self.DatumCode = paramVal 31 | case "rf": 32 | self.Rf, err = strconv.ParseFloat(paramVal, 64) 33 | case "lat_0": 34 | self.Lat0, err = strconv.ParseFloat(paramVal, 64) 35 | self.Lat0 *= deg2rad 36 | case "lat_1": 37 | self.Lat1, err = strconv.ParseFloat(paramVal, 64) 38 | self.Lat1 *= deg2rad 39 | case "lat_2": 40 | self.Lat2, err = strconv.ParseFloat(paramVal, 64) 41 | self.Lat2 *= deg2rad 42 | case "lat_ts": 43 | self.LatTS, err = strconv.ParseFloat(paramVal, 64) 44 | self.LatTS *= deg2rad 45 | case "lon_0": 46 | self.Long0, err = strconv.ParseFloat(paramVal, 64) 47 | self.Long0 *= deg2rad 48 | case "lon_1": 49 | self.Long1, err = strconv.ParseFloat(paramVal, 64) 50 | self.Long1 *= deg2rad 51 | case "lon_2": 52 | self.Long2, err = strconv.ParseFloat(paramVal, 64) 53 | self.Long2 *= deg2rad 54 | case "alpha": 55 | self.Alpha, err = strconv.ParseFloat(paramVal, 64) 56 | self.Alpha *= deg2rad 57 | case "lonc": 58 | self.LongC, err = strconv.ParseFloat(paramVal, 64) 59 | self.LongC *= deg2rad 60 | case "x_0": 61 | self.X0, err = strconv.ParseFloat(paramVal, 64) 62 | case "y_0": 63 | self.Y0, err = strconv.ParseFloat(paramVal, 64) 64 | case "k_0", "k": 65 | self.K0, err = strconv.ParseFloat(paramVal, 64) 66 | case "a": 67 | self.A, err = strconv.ParseFloat(paramVal, 64) 68 | case "b": 69 | self.B, err = strconv.ParseFloat(paramVal, 64) 70 | case "ellps": 71 | self.Ellps = paramVal 72 | case "r_a": 73 | self.Ra = true 74 | case "zone": 75 | self.Zone, err = strconv.ParseFloat(paramVal, 64) 76 | case "south": 77 | self.UTMSouth = true 78 | case "no_defs": 79 | self.NoDefs = true 80 | case "towgs84": 81 | split := strings.Split(paramVal, ",") 82 | self.DatumParams = make([]float64, len(split)) 83 | for i, s := range split { 84 | self.DatumParams[i], err = strconv.ParseFloat(s, 64) 85 | if err != nil { 86 | return nil, err 87 | } 88 | } 89 | case "to_meter": 90 | self.ToMeter, err = strconv.ParseFloat(paramVal, 64) 91 | case "units": 92 | self.Units = paramVal 93 | if u, ok := units[paramVal]; ok { 94 | self.ToMeter = u.to_meter 95 | } 96 | case "from_greenwich": 97 | self.FromGreenwich, err = strconv.ParseFloat(paramVal, 64) 98 | self.FromGreenwich *= deg2rad 99 | case "pm": 100 | if pm, ok := primeMeridian[paramVal]; ok { 101 | self.FromGreenwich = pm 102 | } else { 103 | self.FromGreenwich, err = strconv.ParseFloat(paramVal, 64) 104 | self.FromGreenwich *= deg2rad 105 | } 106 | case "nadgrids": 107 | if paramVal == "@null" { 108 | self.DatumCode = "none" 109 | } else { 110 | self.NADGrids = paramVal 111 | } 112 | case "axis": 113 | legalAxis := "ewnsud" 114 | if len(paramVal) == 3 && strings.Index(legalAxis, paramVal[0:1]) != -1 && 115 | strings.Index(legalAxis, paramVal[1:2]) != -1 && 116 | strings.Index(legalAxis, paramVal[2:3]) != -1 { 117 | self.Axis = paramVal 118 | } 119 | default: 120 | err = fmt.Errorf("proj: invalid field '%s'", paramName) 121 | } 122 | if err != nil { 123 | return nil, err 124 | } 125 | } 126 | if self.DatumCode != "WGS84" { 127 | self.DatumCode = strings.ToLower(self.DatumCode) 128 | } 129 | return self, nil 130 | } 131 | -------------------------------------------------------------------------------- /proj/lcc.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // LCC is a Lambert Conformal Conic projection. 9 | func LCC(this *SR) (forward, inverse Transformer, err error) { 10 | 11 | //double c_lat; /* center latitude */ 12 | //double c_lon; /* center longitude */ 13 | //double lat1; /* first standard parallel */ 14 | //double lat2; /* second standard parallel */ 15 | //double r_maj; /* major axis */ 16 | //double r_min; /* minor axis */ 17 | //double false_east; /* x offset in meters */ 18 | //double false_north; /* y offset in meters */ 19 | 20 | if math.IsNaN(this.Lat2) { 21 | this.Lat2 = this.Lat1 22 | } //if lat2 is not defined 23 | if math.IsNaN(this.K0) { 24 | this.K0 = 1 25 | } 26 | if math.IsNaN(this.X0) { 27 | this.X0 = 0 28 | } 29 | if math.IsNaN(this.Y0) { 30 | this.Y0 = 0 31 | } 32 | // Standard Parallels cannot be equal and on opposite sides of the equator 33 | if math.Abs(this.Lat1+this.Lat2) < epsln { 34 | err = fmt.Errorf("proj.LCC: standard Parallels cannot be equal and on opposite sides of the equator") 35 | return 36 | } 37 | 38 | temp := this.B / this.A 39 | E := math.Sqrt(1 - temp*temp) 40 | 41 | var sin1 = math.Sin(this.Lat1) 42 | var cos1 = math.Cos(this.Lat1) 43 | var ms1 = msfnz(E, sin1, cos1) 44 | var ts1 = tsfnz(E, this.Lat1, sin1) 45 | 46 | var sin2 = math.Sin(this.Lat2) 47 | var cos2 = math.Cos(this.Lat2) 48 | var ms2 = msfnz(E, sin2, cos2) 49 | var ts2 = tsfnz(E, this.Lat2, sin2) 50 | 51 | var ts0 = tsfnz(E, this.Lat0, math.Sin(this.Lat0)) 52 | 53 | var NS float64 54 | if math.Abs(this.Lat1-this.Lat2) > epsln { 55 | NS = math.Log(ms1/ms2) / math.Log(ts1/ts2) 56 | } else { 57 | NS = sin1 58 | } 59 | if math.IsNaN(NS) { 60 | NS = sin1 61 | } 62 | F0 := ms1 / (NS * math.Pow(ts1, NS)) 63 | RH := this.A * F0 * math.Pow(ts0, NS) 64 | //if this.Title == "" { 65 | // this.Title = "Lambert Conformal Conic" 66 | //} 67 | 68 | // Lambert Conformal conic forward equations--mapping lat,long to x,y 69 | // ----------------------------------------------------------------- 70 | forward = func(lon, lat float64) (x, y float64, err error) { 71 | 72 | // singular cases : 73 | if math.Abs(2*math.Abs(lat)-math.Pi) <= epsln { 74 | lat = sign(lat) * (halfPi - 2*epsln) 75 | } 76 | con := math.Abs(math.Abs(lat) - halfPi) 77 | var ts, rh1 float64 78 | if con > epsln { 79 | ts = tsfnz(E, lat, math.Sin(lat)) 80 | rh1 = this.A * F0 * math.Pow(ts, NS) 81 | } else { 82 | con = lat * NS 83 | if con <= 0 { 84 | err = fmt.Errorf("proj.LCC: con <= 0") 85 | return 86 | } 87 | rh1 = 0 88 | } 89 | var theta = NS * adjust_lon(lon-this.Long0) 90 | x = this.K0*(rh1*math.Sin(theta)) + this.X0 91 | y = this.K0*(RH-rh1*math.Cos(theta)) + this.Y0 92 | 93 | return 94 | } 95 | 96 | // Lambert Conformal Conic inverse equations--mapping x,y to lat/long 97 | // ----------------------------------------------------------------- 98 | inverse = func(x, y float64) (lon, lat float64, err error) { 99 | 100 | var rh1, con, ts float64 101 | x = (x - this.X0) / this.K0 102 | y = (RH - (y-this.Y0)/this.K0) 103 | if NS > 0 { 104 | rh1 = math.Sqrt(x*x + y*y) 105 | con = 1 106 | } else { 107 | rh1 = -math.Sqrt(x*x + y*y) 108 | con = -1 109 | } 110 | var theta = 0. 111 | if rh1 != 0 { 112 | theta = math.Atan2((con * x), (con * y)) 113 | } 114 | if (rh1 != 0) || (NS > 0) { 115 | con = 1 / NS 116 | ts = math.Pow((rh1 / (this.A * F0)), con) 117 | lat, err = phi2z(E, ts) 118 | if err != nil { 119 | return 120 | } 121 | } else { 122 | lat = -halfPi 123 | } 124 | lon = adjust_lon(theta/NS + this.Long0) 125 | 126 | return 127 | } 128 | return 129 | } 130 | 131 | func init() { 132 | registerTrans(LCC, "Lambert Tangential Conformal Conic Projection", "Lambert_Conformal_Conic", "Lambert_Conformal_Conic_2SP", "lcc") 133 | } 134 | -------------------------------------------------------------------------------- /area.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "math" 4 | 5 | // Area returns the area of p. The function works correctly for polygons with 6 | // holes, regardless of the winding order of the holes, but will give the wrong 7 | // result for self-intersecting polygons. 8 | func (p Polygon) Area() float64 { 9 | a := 0. 10 | 11 | // Calculate the bounds of all the rings. 12 | bounds := make([]*Bounds, len(p)) 13 | for i, r := range p { 14 | b := NewBounds() 15 | b.extendPoints(r) 16 | bounds[i] = b 17 | } 18 | 19 | for i, r := range p { 20 | a += area(r, i, p, bounds) 21 | } 22 | return a 23 | } 24 | 25 | // area calculates the area of r, where r is a ring within p. 26 | // It returns a negative value if r represents a hole in p. 27 | // It is adapted from http://www.mathopenref.com/coordpolygonarea2.html 28 | // to allow arbitrary winding order. bounds is the bounds of each ring in p. 29 | func area(r []Point, i int, p Polygon, bounds []*Bounds) float64 { 30 | if len(r) < 2 { 31 | return 0 32 | } 33 | highI := len(r) - 1 34 | A := (r[highI].X + 35 | r[0].X) * (r[0].Y - r[highI].Y) 36 | for ii := 0; ii < highI; ii++ { 37 | A += (r[ii].X + 38 | r[ii+1].X) * (r[ii+1].Y - r[ii].Y) 39 | } 40 | A = math.Abs(A / 2.) 41 | // check whether all of the points on this ring are inside 42 | // the polygon. 43 | if len(p) == 1 { 44 | return A // This is not a hole. 45 | } 46 | pWithoutRing := make(Polygon, len(p)) 47 | copy(pWithoutRing, p) 48 | pWithoutRing = Polygon(append(pWithoutRing[:i], pWithoutRing[i+1:]...)) 49 | boundsWithoutRing := make([]*Bounds, len(p)) 50 | copy(boundsWithoutRing, bounds) 51 | boundsWithoutRing = append(boundsWithoutRing[:i], boundsWithoutRing[i+1:]...) 52 | 53 | for _, pp := range r { 54 | in := pointInPolygon(pp, pWithoutRing, boundsWithoutRing) 55 | if in == OnEdge { 56 | continue // It is not clear whether this is a hole or not. 57 | } else if in == Outside { 58 | return A // This is not a hole. 59 | } 60 | return -A // This is a hole 61 | } 62 | 63 | // All of the points on this ring are on the edge of the polygon. In this 64 | // case we check if this ring exactly matches, and therefore cancels out, 65 | // any of the other rings. 66 | matches := 0 67 | for _, rr := range pWithoutRing { 68 | if pointsSimilar(r, rr, 0) { 69 | matches++ 70 | } 71 | } 72 | if matches%2 == 1 { 73 | return 0 // There is an odd number of matches so the area cancels out. 74 | } 75 | // If we get here there is an even number of matches. If the polygon is not 76 | // self-intersecting (only self-touching) that means this is a hole. 77 | // The algorithm is not guaranteed to work with self-intersecting polygons. 78 | return -A // This is a hole 79 | } 80 | 81 | func (p Polygon) ringBounds() []*Bounds { 82 | bounds := make([]*Bounds, len(p)) 83 | for i, r := range p { 84 | pgBounds := NewBounds() 85 | pgBounds.extendPoints(r) 86 | bounds[i] = pgBounds 87 | } 88 | return bounds 89 | } 90 | 91 | // see http://www.mathopenref.com/coordpolygonarea2.html 92 | func signedarea(polygon []Point) float64 { 93 | if len(polygon) < 2 { 94 | return 0 95 | } 96 | highI := len(polygon) - 1 97 | A := (polygon[highI].X + 98 | polygon[0].X) * (polygon[0].Y - polygon[highI].Y) 99 | for i := 0; i < highI; i++ { 100 | A += (polygon[i].X + 101 | polygon[i+1].X) * (polygon[i+1].Y - polygon[i].Y) 102 | } 103 | return A / 2. 104 | } 105 | 106 | // Centroid calculates the centroid of p, from 107 | // wikipedia: http://en.wikipedia.org/wiki/Centroid#Centroid_of_polygon. 108 | // The polygon can have holes, but each ring must be closed (i.e., 109 | // p[0] == p[n-1], where the ring has n points) and must not be 110 | // self-intersecting. 111 | // The algorithm will not check to make sure the holes are 112 | // actually inside the outer rings. 113 | // This has not been thoroughly tested. 114 | func (p Polygon) Centroid() Point { 115 | var A, xA, yA float64 116 | for _, r := range p { 117 | a := signedarea(r) 118 | cx, cy := 0., 0. 119 | if r[len(r)-1] != r[0] { 120 | r = append(r, r[0]) 121 | } 122 | for i := 0; i < len(r)-1; i++ { 123 | cx += (r[i].X + r[i+1].X) * 124 | (r[i].X*r[i+1].Y - r[i+1].X*r[i].Y) 125 | cy += (r[i].Y + r[i+1].Y) * 126 | (r[i].X*r[i+1].Y - r[i+1].X*r[i].Y) 127 | } 128 | cx /= 6 * a 129 | cy /= 6 * a 130 | A += a 131 | xA += cx * a 132 | yA += cy * a 133 | } 134 | return Point{X: xA / A, Y: yA / A} 135 | } 136 | -------------------------------------------------------------------------------- /proj/Proj.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | import ( 4 | "fmt" 5 | "gonum.org/v1/gonum/floats/scalar" 6 | "math" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | // A Transformer takes input coordinates and returns output coordinates and an error. 12 | type Transformer func(X, Y float64) (x, y float64, err error) 13 | 14 | // A TransformerFunc creates forward and inverse Transformers from a projection. 15 | type TransformerFunc func(*SR) (forward, inverse Transformer, err error) 16 | 17 | var projections map[string]TransformerFunc 18 | 19 | // SR holds information about a spatial reference (projection). 20 | type SR struct { 21 | Name, Title string 22 | SRSCode string 23 | DatumCode string 24 | Rf float64 25 | Lat0, Lat1, Lat2, LatTS float64 26 | Long0, Long1, Long2, LongC float64 27 | Alpha float64 28 | X0, Y0, K0, K float64 29 | A, A2, B, B2 float64 30 | Ra bool 31 | Zone float64 32 | UTMSouth bool 33 | DatumParams []float64 34 | ToMeter float64 35 | Units string 36 | FromGreenwich float64 37 | NADGrids string 38 | Axis string 39 | local bool 40 | sphere bool 41 | Ellps string 42 | EllipseName string 43 | Es float64 44 | E float64 45 | Ep2 float64 46 | DatumName string 47 | NoDefs bool 48 | datum *datum 49 | Czech bool 50 | } 51 | 52 | // NewSR initializes a SR object and sets fields to default values. 53 | func NewSR() *SR { 54 | p := new(SR) 55 | // Initialize floats to NaN. 56 | v := reflect.ValueOf(p).Elem() 57 | for i := 0; i < v.NumField(); i++ { 58 | f := v.Field(i) 59 | ft := f.Type().Kind() 60 | if ft == reflect.Float64 { 61 | f.SetFloat(math.NaN()) 62 | } 63 | } 64 | p.ToMeter = 1. 65 | return p 66 | } 67 | 68 | func registerTrans(proj TransformerFunc, names ...string) { 69 | if projections == nil { 70 | projections = make(map[string]TransformerFunc) 71 | } 72 | for _, n := range names { 73 | projections[strings.ToLower(n)] = proj 74 | } 75 | } 76 | 77 | // Transformers returns forward and inverse transformation functions for 78 | // this projection. 79 | func (sr *SR) Transformers() (forward, inverse Transformer, err error) { 80 | t, ok := projections[strings.ToLower(sr.Name)] 81 | if !ok { 82 | err = fmt.Errorf("in proj.Proj.TransformFuncs, could not find "+ 83 | "transformer for %s", sr.Name) 84 | return 85 | } 86 | forward, inverse, err = t(sr) 87 | return 88 | } 89 | 90 | // Equal determines whether spatial references sr and sr2 are equal to within ulp 91 | // floating point units in the last place. 92 | func (sr *SR) Equal(sr2 *SR, ulp uint) bool { 93 | v1 := reflect.ValueOf(sr).Elem() 94 | v2 := reflect.ValueOf(sr2).Elem() 95 | return equal(v1, v2, ulp) 96 | } 97 | 98 | // equal determines whether two values are equal to each other within ulp 99 | func equal(v1, v2 reflect.Value, ulp uint) bool { 100 | for i := 0; i < v1.NumField(); i++ { 101 | f1 := v1.Field(i) 102 | f2 := v2.Field(i) 103 | ft := f1.Type().Kind() 104 | switch ft { 105 | case reflect.Float64: 106 | fv1 := f1.Float() 107 | fv2 := f2.Float() 108 | if math.IsNaN(fv1) != math.IsNaN(fv2) { 109 | return false 110 | } 111 | if !math.IsNaN(fv1) && !scalar.EqualWithinULP(fv1, fv2, ulp) { 112 | return false 113 | } 114 | case reflect.Int: 115 | if f1.Int() != f2.Int() { 116 | return false 117 | } 118 | case reflect.Bool: 119 | if f1.Bool() != f2.Bool() { 120 | return false 121 | } 122 | case reflect.Ptr: 123 | if !equal(reflect.Indirect(f1), reflect.Indirect(f2), ulp) { 124 | return false 125 | } 126 | case reflect.String: 127 | if f1.String() != f2.String() { 128 | return false 129 | } 130 | case reflect.Slice: 131 | for i := 0; i < f1.Len(); i++ { 132 | if !scalar.EqualWithinULP(f1.Index(i).Float(), f2.Index(i).Float(), ulp) { 133 | return false 134 | } 135 | } 136 | default: 137 | panic(fmt.Errorf("unsupported type %s", ft)) 138 | } 139 | } 140 | return true 141 | } 142 | -------------------------------------------------------------------------------- /intersection.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "math" 4 | 5 | var nanPoint Point 6 | 7 | func init() { 8 | nanPoint = Point{X: math.NaN(), Y: math.NaN()} 9 | } 10 | 11 | // Modified from package github.com/akavel/polyclip-go. 12 | // Copyright (c) 2011 Mateusz Czapliński (Go port) 13 | // Copyright (c) 2011 Mahir Iqbal (as3 version) 14 | // Permission is hereby granted, free of charge, to any person obtaining a copy 15 | // of this software and associated documentation files (the "Software"), to deal 16 | // in the Software without restriction, including without limitation the rights 17 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | // copies of the Software, and to permit persons to whom the Software is 19 | // furnished to do so, subject to the following conditions: 20 | // 21 | // The above copyright notice and this permission notice shall be included in 22 | // all copies or substantial portions of the Software. 23 | // 24 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | // THE SOFTWARE. 31 | 32 | // based on http://code.google.com/p/as3polyclip/ (MIT licensed) 33 | // and code by Martínez et al: http://wwwdi.ujaen.es/~fmartin/bool_op.html (public domain) 34 | func findIntersection(seg0, seg1 segment) (int, Point, Point) { 35 | pi0 := nanPoint 36 | pi1 := nanPoint 37 | p0 := seg0.start 38 | d0 := Point{seg0.end.X - p0.X, seg0.end.Y - p0.Y} 39 | p1 := seg1.start 40 | d1 := Point{seg1.end.X - p1.X, seg1.end.Y - p1.Y} 41 | sqrEpsilon := 0. // was 1e-3 earlier 42 | E := Point{p1.X - p0.X, p1.Y - p0.Y} 43 | kross := d0.X*d1.Y - d0.Y*d1.X 44 | sqrKross := kross * kross 45 | sqrLen0 := lengthToOrigin(d0) 46 | sqrLen1 := lengthToOrigin(d1) 47 | 48 | if sqrKross > sqrEpsilon*sqrLen0*sqrLen1 { 49 | // lines of the segments are not parallel 50 | s := (E.X*d1.Y - E.Y*d1.X) / kross 51 | if s < 0 || s > 1 { 52 | return 0, Point{}, Point{} 53 | } 54 | t := (E.X*d0.Y - E.Y*d0.X) / kross 55 | if t < 0 || t > 1 { 56 | return 0, nanPoint, nanPoint 57 | } 58 | // intersection of lines is a point an each segment [MC: ?] 59 | pi0.X = p0.X + s*d0.X 60 | pi0.Y = p0.Y + s*d0.Y 61 | 62 | // [MC: commented fragment removed] 63 | 64 | return 1, pi0, nanPoint 65 | } 66 | 67 | // lines of the segments are parallel 68 | sqrLenE := lengthToOrigin(E) 69 | kross = E.X*d0.Y - E.Y*d0.X 70 | sqrKross = kross * kross 71 | if sqrKross > sqrEpsilon*sqrLen0*sqrLenE { 72 | // lines of the segment are different 73 | return 0, nanPoint, nanPoint 74 | } 75 | 76 | // Lines of the segment are the same. Need to test for overlap of segments. 77 | // s0 = Dot (D0, E) * sqrLen0 78 | s0 := (d0.X*E.X + d0.Y*E.Y) / sqrLen0 79 | // s1 = s0 + Dot (D0, D1) * sqrLen0 80 | s1 := s0 + (d0.X*d1.X+d0.Y*d1.Y)/sqrLen0 81 | smin := math.Min(s0, s1) 82 | smax := math.Max(s0, s1) 83 | w := make([]float64, 0, 2) 84 | imax := findIntersection2(0.0, 1.0, smin, smax, &w) 85 | 86 | if imax > 0 { 87 | pi0.X = p0.X + w[0]*d0.X 88 | pi0.Y = p0.Y + w[0]*d0.Y 89 | 90 | // [MC: commented fragment removed] 91 | 92 | if imax > 1 { 93 | pi1.X = p0.X + w[1]*d0.X 94 | pi1.Y = p0.Y + w[1]*d0.Y 95 | } 96 | } 97 | 98 | return imax, pi0, pi1 99 | } 100 | 101 | func findIntersection2(u0, u1, v0, v1 float64, w *[]float64) int { 102 | if u1 < v0 || u0 > v1 { 103 | return 0 104 | } 105 | if u1 == v0 { 106 | *w = append(*w, u1) 107 | return 1 108 | } 109 | 110 | // u1 > v0 111 | 112 | if u0 == v1 { 113 | *w = append(*w, u0) 114 | return 1 115 | } 116 | 117 | // u0 < v1 118 | 119 | if u0 < v0 { 120 | *w = append(*w, v0) 121 | } else { 122 | *w = append(*w, u0) 123 | } 124 | if u1 > v1 { 125 | *w = append(*w, v1) 126 | } else { 127 | *w = append(*w, u1) 128 | } 129 | return 2 130 | } 131 | 132 | // Length returns distance from p to point (0, 0). 133 | func lengthToOrigin(p Point) float64 { 134 | return math.Sqrt(p.X*p.X + p.Y*p.Y) 135 | } 136 | 137 | // Used to represent an edge of a polygon. 138 | type segment struct { 139 | start, end Point 140 | } 141 | -------------------------------------------------------------------------------- /polygonop_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "testing" 4 | 5 | func TestPolygonOp(t *testing.T) { 6 | tests := []struct { 7 | subject, clipping Polygonal 8 | intersection, union, difference, xor Polygonal 9 | }{ 10 | { 11 | subject: Polygon{{ 12 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 13 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 14 | Point{X: 0, Y: 0}}}, 15 | clipping: Polygon{{ 16 | Point{X: -1, Y: -1}, Point{X: 1, Y: -1}, 17 | Point{X: 1, Y: 1}, Point{X: -1, Y: 1}, 18 | Point{X: -1, Y: -1}}}, 19 | intersection: Polygon{{ 20 | Point{X: 0, Y: 0}, Point{X: 1, Y: 0}, 21 | Point{X: 1, Y: 1}, Point{X: 0, Y: 1}, 22 | Point{X: 0, Y: 0}}}, 23 | union: Polygon{{ 24 | Point{X: -1, Y: -1}, Point{X: 1, Y: -1}, 25 | Point{X: 1, Y: 0}, Point{X: 2, Y: 0}, 26 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 27 | Point{X: 0, Y: 1}, Point{X: -1, Y: 1}, 28 | Point{X: -1, Y: -1}}}, 29 | difference: Polygon{{ 30 | Point{X: 1, Y: 0}, Point{X: 2, Y: 0}, 31 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 32 | Point{X: 0, Y: 1}, Point{X: 1, Y: 1}, 33 | Point{X: 1, Y: 0}}}, 34 | xor: Polygon{ 35 | { 36 | Point{X: 1, Y: 0}, Point{X: 2, Y: 0}, 37 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 38 | Point{X: 0, Y: 1}, Point{X: 1, Y: 1}, 39 | Point{X: 1, Y: 0}, 40 | }, 41 | { 42 | Point{X: -1, Y: -1}, Point{X: 1, Y: -1}, 43 | Point{X: 1, Y: 0}, Point{X: 0, Y: 0}, 44 | Point{X: 0, Y: 1}, Point{X: -1, Y: 1}, 45 | Point{X: -1, Y: -1}, 46 | }, 47 | }, 48 | }, 49 | { 50 | subject: MultiPolygon{ 51 | {{ 52 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 53 | Point{X: 2, Y: 2}, Point{X: 0, Y: 2}, 54 | Point{X: 0, Y: 0}}}, 55 | {{ 56 | Point{X: 4, Y: 0}, Point{X: 6, Y: 0}, 57 | Point{X: 6, Y: 2}, Point{X: 4, Y: 2}, 58 | Point{X: 4, Y: 0}}}, 59 | }, 60 | clipping: Polygon{{ 61 | Point{X: 1, Y: 1}, Point{X: 5, Y: 1}, 62 | Point{X: 5, Y: 3}, Point{X: 1, Y: 3}, 63 | Point{X: 1, Y: 1}}}, 64 | intersection: Polygon{ 65 | { 66 | Point{X: 1, Y: 1}, Point{X: 2, Y: 1}, 67 | Point{X: 2, Y: 2}, Point{X: 1, Y: 2}, 68 | Point{X: 1, Y: 1}, 69 | }, 70 | { 71 | Point{X: 4, Y: 1}, Point{X: 5, Y: 1}, 72 | Point{X: 5, Y: 2}, Point{X: 4, Y: 2}, 73 | Point{X: 4, Y: 1}, 74 | }, 75 | }, 76 | union: Polygon{{ 77 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 78 | Point{X: 2, Y: 1}, Point{X: 4, Y: 1}, 79 | Point{X: 4, Y: 0}, Point{X: 6, Y: 0}, 80 | Point{X: 6, Y: 2}, Point{X: 5, Y: 2}, 81 | Point{X: 5, Y: 3}, Point{X: 1, Y: 3}, 82 | Point{X: 1, Y: 2}, Point{X: 0, Y: 2}, 83 | Point{X: 0, Y: 0}}}, 84 | difference: Polygon{ 85 | { 86 | Point{X: 0, Y: 0}, Point{X: 2, Y: 0}, 87 | Point{X: 2, Y: 1}, Point{X: 1, Y: 1}, 88 | Point{X: 1, Y: 2}, Point{X: 0, Y: 2}, 89 | Point{X: 0, Y: 0}, 90 | }, 91 | { 92 | Point{X: 4, Y: 0}, Point{X: 6, Y: 0}, 93 | Point{X: 6, Y: 2}, Point{X: 5, Y: 2}, 94 | Point{X: 5, Y: 1}, Point{X: 4, Y: 1}, 95 | Point{X: 4, Y: 0}, 96 | }}, 97 | xor: Polygon{ 98 | { 99 | Point{X: 1, Y: 1}, 100 | Point{X: 1, Y: 2}, 101 | Point{X: 0, Y: 2}, 102 | Point{X: 0, Y: 0}, 103 | Point{X: 2, Y: 0}, 104 | Point{X: 2, Y: 1}, 105 | Point{X: 1, Y: 1}, 106 | }, 107 | { 108 | Point{X: 4, Y: 2}, 109 | Point{X: 4, Y: 1}, 110 | Point{X: 5, Y: 1}, 111 | Point{X: 5, Y: 2}, 112 | Point{X: 4, Y: 2}, 113 | }, 114 | { 115 | Point{X: 5, Y: 2}, 116 | Point{X: 5, Y: 3}, 117 | Point{X: 1, Y: 3}, 118 | Point{X: 1, Y: 2}, 119 | Point{X: 2, Y: 2}, 120 | Point{X: 2, Y: 1}, 121 | Point{X: 4, Y: 1}, 122 | Point{X: 4, Y: 0}, 123 | Point{X: 6, Y: 0}, 124 | Point{X: 6, Y: 2}, 125 | Point{X: 5, Y: 2}, 126 | }, 127 | }, 128 | }, 129 | } 130 | for i, test := range tests { 131 | intersection := test.subject.Intersection(test.clipping) 132 | if !intersection.Similar(test.intersection, 1.e-9) { 133 | t.Errorf("%d: intersection expected %v, got %v", i, test.intersection, intersection) 134 | } 135 | union := test.subject.Union(test.clipping) 136 | if !union.Similar(test.union, 1.e-9) { 137 | t.Errorf("%d: union expected %v, got %v", i, test.union, union) 138 | } 139 | difference := test.subject.Difference(test.clipping) 140 | if !difference.Similar(test.difference, 1.e-9) { 141 | t.Errorf("%d: difference expected %v, got %v", i, test.difference, difference) 142 | } 143 | xor := test.subject.XOr(test.clipping) 144 | if !xor.Similar(test.xor, 1.e-9) { 145 | t.Errorf("%d: xor expected %v, got %v", i, test.xor, xor) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /bounds_test.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestBounds_Within(t *testing.T) { 10 | b := &Bounds{ 11 | Min: Point{0, 0}, 12 | Max: Point{1, 1}, 13 | } 14 | tests := []struct { 15 | p Polygonal 16 | in WithinStatus 17 | }{ 18 | { 19 | p: &Bounds{ 20 | Min: Point{0, 0}, 21 | Max: Point{1, 1}, 22 | }, 23 | in: OnEdge, 24 | }, 25 | { 26 | p: &Bounds{ 27 | Min: Point{0.5, 0.5}, 28 | Max: Point{1, 1}, 29 | }, 30 | in: Outside, 31 | }, 32 | { 33 | p: &Bounds{ 34 | Min: Point{0.5, 0.5}, 35 | Max: Point{0.6, 0.6}, 36 | }, 37 | in: Outside, 38 | }, 39 | { 40 | p: &Bounds{ 41 | Min: Point{0.5, 0.5}, 42 | Max: Point{2, 2}, 43 | }, 44 | in: Outside, 45 | }, 46 | { 47 | p: &Bounds{ 48 | Min: Point{1, 1}, 49 | Max: Point{2, 2}, 50 | }, 51 | in: Outside, 52 | }, 53 | { 54 | p: &Bounds{ 55 | Min: Point{1.5, 1.5}, 56 | Max: Point{2, 2}, 57 | }, 58 | in: Outside, 59 | }, 60 | { 61 | p: &Bounds{ 62 | Min: Point{-1, -1}, 63 | Max: Point{1, 1}, 64 | }, 65 | in: Inside, 66 | }, 67 | { 68 | p: &Bounds{ 69 | Min: Point{-1, -1}, 70 | Max: Point{2, 2}, 71 | }, 72 | in: Inside, 73 | }, 74 | { 75 | p: Polygon{{{0, 0}, {1, 1}, {0, 1}}}, 76 | in: Inside, 77 | }, 78 | { 79 | p: Polygon{{{0, 0}, {1, 1}, {-1, 1}}}, 80 | in: Inside, 81 | }, 82 | { 83 | p: Polygon{{{0.2, 0.2}, {0.7, 0.7}, {0.2, 0.7}}}, 84 | in: Outside, 85 | }, 86 | } 87 | for i, test := range tests { 88 | t.Run(fmt.Sprint(i), func(t *testing.T) { 89 | in := b.Within(test.p) 90 | if in != test.in { 91 | t.Errorf("%v != %v", in, test.in) 92 | } 93 | }) 94 | } 95 | } 96 | 97 | func TestBounds_Polygons(t *testing.T) { 98 | b := &Bounds{ 99 | Min: Point{0, 0}, 100 | Max: Point{1, 1}, 101 | } 102 | p := b.Polygons() 103 | want := []Polygon{{{{X: 0, Y: 0}, {X: 1, Y: 0}, {X: 1, Y: 1}, {X: 0, Y: 1}}}} 104 | if !reflect.DeepEqual(p, want) { 105 | t.Errorf("%v != %v", p, want) 106 | } 107 | } 108 | 109 | func TestBounds_Intersection(t *testing.T) { 110 | b := &Bounds{ 111 | Min: Point{0, 0}, 112 | Max: Point{1, 1}, 113 | } 114 | tests := []struct { 115 | p Polygonal 116 | i Polygonal 117 | }{ 118 | { 119 | p: &Bounds{ 120 | Min: Point{0, 0}, 121 | Max: Point{1, 1}, 122 | }, 123 | i: b, 124 | }, 125 | { 126 | p: &Bounds{ 127 | Min: Point{0.5, 0.5}, 128 | Max: Point{1, 1}, 129 | }, 130 | i: &Bounds{ 131 | Min: Point{0.5, 0.5}, 132 | Max: Point{1, 1}, 133 | }, 134 | }, 135 | { 136 | p: &Bounds{ 137 | Min: Point{0.5, 0.5}, 138 | Max: Point{0.6, 0.6}, 139 | }, 140 | i: &Bounds{ 141 | Min: Point{0.5, 0.5}, 142 | Max: Point{0.6, 0.6}, 143 | }, 144 | }, 145 | { 146 | p: &Bounds{ 147 | Min: Point{0.5, 0.5}, 148 | Max: Point{2, 2}, 149 | }, 150 | i: &Bounds{ 151 | Min: Point{0.5, 0.5}, 152 | Max: Point{1, 1}, 153 | }, 154 | }, 155 | { 156 | p: &Bounds{ 157 | Min: Point{1, 1}, 158 | Max: Point{2, 2}, 159 | }, 160 | i: Polygon{}, 161 | }, 162 | { 163 | p: &Bounds{ 164 | Min: Point{1.5, 1.5}, 165 | Max: Point{2, 2}, 166 | }, 167 | i: Polygon{}, 168 | }, 169 | { 170 | p: &Bounds{ 171 | Min: Point{-1, -1}, 172 | Max: Point{1, 1}, 173 | }, 174 | i: b, 175 | }, 176 | { 177 | p: &Bounds{ 178 | Min: Point{-1, -1}, 179 | Max: Point{2, 2}, 180 | }, 181 | i: b, 182 | }, 183 | { 184 | p: Polygon{{{0, 0}, {1, 1}, {0, 1}}}, 185 | i: Polygon{{{0, 0}, {1, 1}, {0, 1}}}, 186 | }, 187 | { 188 | p: Polygon{{{0, 0}, {1, 1}, {-1, 1}}}, 189 | i: Polygon{{{0, 1}, {0, 0}, {1, 1}, {0, 1}}}, 190 | }, 191 | { 192 | p: Polygon{{{0.2, 0.2}, {0.7, 0.7}, {0.2, 0.7}}}, 193 | i: Polygon{{{0.2, 0.2}, {0.7, 0.7}, {0.2, 0.7}}}, 194 | }, 195 | } 196 | for i, test := range tests { 197 | t.Run(fmt.Sprint(i), func(t *testing.T) { 198 | i := b.Intersection(test.p) 199 | if i == nil && test.i.Len() == 0 { 200 | return 201 | } 202 | if !reflect.DeepEqual(i, test.i) { 203 | t.Errorf("%v != %v", i, test.i) 204 | } 205 | }) 206 | } 207 | } 208 | 209 | func TestBounds_Area(t *testing.T) { 210 | b := &Bounds{ 211 | Min: Point{0, 0}, 212 | Max: Point{1, 1}, 213 | } 214 | a := b.Area() 215 | if a != 1 { 216 | t.Errorf("%v != %v", a, 1) 217 | } 218 | } 219 | 220 | func TestBounds_Centroid(t *testing.T) { 221 | b := &Bounds{ 222 | Min: Point{0, 0}, 223 | Max: Point{1, 1}, 224 | } 225 | c := b.Centroid() 226 | want := Point{0.5, 0.5} 227 | if !reflect.DeepEqual(c, want) { 228 | t.Errorf("%v != %v", c, want) 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /encoding/wkb/wkb.go: -------------------------------------------------------------------------------- 1 | package wkb 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | 10 | "github.com/ctessum/geom" 11 | ) 12 | 13 | const ( 14 | wkbXDR = 0 15 | wkbNDR = 1 16 | ) 17 | 18 | const ( 19 | wkbPoint = 1 20 | wkbLineString = 2 21 | wkbPolygon = 3 22 | wkbMultiPoint = 4 23 | wkbMultiLineString = 5 24 | wkbMultiPolygon = 6 25 | wkbGeometryCollection = 7 26 | wkbPolyhedralSurface = 15 27 | wkbTIN = 16 28 | wkbTriangle = 17 29 | ) 30 | 31 | var ( 32 | XDR = binary.BigEndian 33 | NDR = binary.LittleEndian 34 | ) 35 | 36 | type UnexpectedGeometryError struct { 37 | Geom geom.Geom 38 | } 39 | 40 | func (e UnexpectedGeometryError) Error() string { 41 | return fmt.Sprintf("wkb: unexpected geometry %v", e.Geom) 42 | } 43 | 44 | type UnsupportedGeometryError struct { 45 | Type reflect.Type 46 | } 47 | 48 | func (e UnsupportedGeometryError) Error() string { 49 | return "wkb: unsupported type: " + e.Type.String() 50 | } 51 | 52 | type wkbReader func(io.Reader, binary.ByteOrder) (geom.Geom, error) 53 | 54 | var wkbReaders map[uint32]wkbReader 55 | 56 | func init() { 57 | wkbReaders = make(map[uint32]wkbReader) 58 | wkbReaders[wkbPoint] = pointReader 59 | wkbReaders[wkbLineString] = lineStringReader 60 | wkbReaders[wkbPolygon] = polygonReader 61 | wkbReaders[wkbMultiPoint] = multiPointReader 62 | wkbReaders[wkbMultiLineString] = multiLineStringReader 63 | wkbReaders[wkbMultiPolygon] = multiPolygonReader 64 | wkbReaders[wkbGeometryCollection] = geometryCollectionReader 65 | } 66 | 67 | func Read(r io.Reader) (geom.Geom, error) { 68 | 69 | var wkbByteOrder uint8 70 | if err := binary.Read(r, binary.LittleEndian, &wkbByteOrder); err != nil { 71 | return nil, err 72 | } 73 | var byteOrder binary.ByteOrder 74 | switch wkbByteOrder { 75 | case wkbXDR: 76 | byteOrder = binary.BigEndian 77 | case wkbNDR: 78 | byteOrder = binary.LittleEndian 79 | default: 80 | return nil, fmt.Errorf("invalid byte order %v", wkbByteOrder) 81 | } 82 | 83 | var wkbGeometryType uint32 84 | if err := binary.Read(r, byteOrder, &wkbGeometryType); err != nil { 85 | return nil, err 86 | } 87 | 88 | if reader, ok := wkbReaders[wkbGeometryType]; ok { 89 | return reader(r, byteOrder) 90 | } else { 91 | return nil, fmt.Errorf("unsupported geometry type %v", wkbGeometryType) 92 | } 93 | 94 | } 95 | 96 | func Decode(buf []byte) (geom.Geom, error) { 97 | return Read(bytes.NewBuffer(buf)) 98 | } 99 | 100 | func writeMany(w io.Writer, byteOrder binary.ByteOrder, data ...interface{}) error { 101 | for _, datum := range data { 102 | if err := binary.Write(w, byteOrder, datum); err != nil { 103 | return err 104 | } 105 | } 106 | return nil 107 | } 108 | 109 | func Write(w io.Writer, byteOrder binary.ByteOrder, g geom.Geom) error { 110 | var wkbByteOrder uint8 111 | switch byteOrder { 112 | case XDR: 113 | wkbByteOrder = wkbXDR 114 | case NDR: 115 | wkbByteOrder = wkbNDR 116 | default: 117 | return fmt.Errorf("unsupported byte order %v", byteOrder) 118 | } 119 | if err := binary.Write(w, byteOrder, wkbByteOrder); err != nil { 120 | return err 121 | } 122 | var wkbGeometryType uint32 123 | switch g.(type) { 124 | case geom.Point: 125 | wkbGeometryType = wkbPoint 126 | case geom.LineString: 127 | wkbGeometryType = wkbLineString 128 | case geom.Polygon: 129 | wkbGeometryType = wkbPolygon 130 | case geom.MultiPoint: 131 | wkbGeometryType = wkbMultiPoint 132 | case geom.MultiLineString: 133 | wkbGeometryType = wkbMultiLineString 134 | case geom.MultiPolygon: 135 | wkbGeometryType = wkbMultiPolygon 136 | case geom.GeometryCollection: 137 | wkbGeometryType = wkbGeometryCollection 138 | default: 139 | return &UnsupportedGeometryError{reflect.TypeOf(g)} 140 | } 141 | if err := binary.Write(w, byteOrder, wkbGeometryType); err != nil { 142 | return err 143 | } 144 | switch g.(type) { 145 | case geom.Point: 146 | return writePoint(w, byteOrder, g.(geom.Point)) 147 | case geom.LineString: 148 | return writeLineString(w, byteOrder, g.(geom.LineString)) 149 | case geom.Polygon: 150 | return writePolygon(w, byteOrder, g.(geom.Polygon)) 151 | case geom.MultiPoint: 152 | return writeMultiPoint(w, byteOrder, g.(geom.MultiPoint)) 153 | case geom.MultiLineString: 154 | return writeMultiLineString(w, byteOrder, g.(geom.MultiLineString)) 155 | case geom.MultiPolygon: 156 | return writeMultiPolygon(w, byteOrder, g.(geom.MultiPolygon)) 157 | case geom.GeometryCollection: 158 | return writeGeometryCollection(w, byteOrder, g.(geom.GeometryCollection)) 159 | default: 160 | return &UnsupportedGeometryError{reflect.TypeOf(g)} 161 | } 162 | } 163 | 164 | func Encode(g geom.Geom, byteOrder binary.ByteOrder) ([]byte, error) { 165 | w := bytes.NewBuffer(nil) 166 | if err := Write(w, byteOrder, g); err != nil { 167 | return nil, err 168 | } 169 | return w.Bytes(), nil 170 | } 171 | -------------------------------------------------------------------------------- /encoding/geojson/decode.go: -------------------------------------------------------------------------------- 1 | package geojson 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ctessum/geom" 7 | ) 8 | 9 | func decodeCoordinates(jsonCoordinates interface{}) []float64 { 10 | array, ok := jsonCoordinates.([]interface{}) 11 | if !ok { 12 | panic(&InvalidGeometryError{}) 13 | } 14 | coordinates := make([]float64, len(array)) 15 | for i, element := range array { 16 | var ok bool 17 | if coordinates[i], ok = element.(float64); !ok { 18 | panic(&InvalidGeometryError{}) 19 | } 20 | } 21 | return coordinates 22 | } 23 | 24 | func decodeCoordinates2(jsonCoordinates interface{}) [][]float64 { 25 | array, ok := jsonCoordinates.([]interface{}) 26 | if !ok { 27 | panic(&InvalidGeometryError{}) 28 | } 29 | coordinates := make([][]float64, len(array)) 30 | for i, element := range array { 31 | coordinates[i] = decodeCoordinates(element) 32 | } 33 | return coordinates 34 | } 35 | 36 | func decodeCoordinates3(jsonCoordinates interface{}) [][][]float64 { 37 | array, ok := jsonCoordinates.([]interface{}) 38 | if !ok { 39 | panic(&InvalidGeometryError{}) 40 | } 41 | coordinates := make([][][]float64, len(array)) 42 | for i, element := range array { 43 | coordinates[i] = decodeCoordinates2(element) 44 | } 45 | return coordinates 46 | } 47 | 48 | func decodeCoordinates4(jsonCoordinates interface{}) [][][][]float64 { 49 | array, ok := jsonCoordinates.([]interface{}) 50 | if !ok { 51 | panic(&InvalidGeometryError{}) 52 | } 53 | coordinates := make([][][][]float64, len(array)) 54 | for i, element := range array { 55 | coordinates[i] = decodeCoordinates3(element) 56 | } 57 | return coordinates 58 | } 59 | 60 | func makeLinearRing(coordinates [][]float64) geom.Path { 61 | points := make(geom.Path, len(coordinates)) 62 | for i, element := range coordinates { 63 | if len(element) == 2 { 64 | points[i].X = element[0] 65 | points[i].Y = element[1] 66 | } else { 67 | panic(&InvalidGeometryError{}) 68 | } 69 | } 70 | return points 71 | } 72 | 73 | func makeLinearRings(coordinates [][][]float64) []geom.Path { 74 | pointss := make([]geom.Path, len(coordinates)) 75 | for i, element := range coordinates { 76 | pointss[i] = makeLinearRing(element) 77 | } 78 | return pointss 79 | } 80 | 81 | func doFromGeoJSON(g *Geometry) geom.Geom { 82 | switch g.Type { 83 | case "Point": 84 | coordinates := decodeCoordinates(g.Coordinates) 85 | switch len(coordinates) { 86 | case 2: 87 | return geom.Point{coordinates[0], coordinates[1]} 88 | default: 89 | panic(&InvalidGeometryError{}) 90 | } 91 | case "MultiPoint": 92 | coordinates := decodeCoordinates2(g.Coordinates) 93 | if len(coordinates) == 0 { 94 | panic(&InvalidGeometryError{}) 95 | } 96 | switch len(coordinates[0]) { 97 | case 2: 98 | return geom.MultiPoint(makeLinearRing(coordinates)) 99 | default: 100 | panic(&InvalidGeometryError{}) 101 | } 102 | case "LineString": 103 | coordinates := decodeCoordinates2(g.Coordinates) 104 | if len(coordinates) == 0 { 105 | panic(&InvalidGeometryError{}) 106 | } 107 | switch len(coordinates[0]) { 108 | case 2: 109 | return geom.LineString(makeLinearRing(coordinates)) 110 | default: 111 | panic(&InvalidGeometryError{}) 112 | } 113 | case "MultiLineString": 114 | coordinates := decodeCoordinates3(g.Coordinates) 115 | if len(coordinates) == 0 || len(coordinates[0]) == 0 { 116 | panic(&InvalidGeometryError{}) 117 | } 118 | switch len(coordinates[0][0]) { 119 | case 2: 120 | multiLineString := make(geom.MultiLineString, len(coordinates)) 121 | for i, coord := range coordinates { 122 | multiLineString[i] = geom.LineString(makeLinearRing(coord)) 123 | } 124 | return multiLineString 125 | default: 126 | panic(&InvalidGeometryError{}) 127 | } 128 | case "Polygon": 129 | coordinates := decodeCoordinates3(g.Coordinates) 130 | if len(coordinates) == 0 || len(coordinates[0]) == 0 { 131 | panic(&InvalidGeometryError{}) 132 | } 133 | switch len(coordinates[0][0]) { 134 | case 2: 135 | return geom.Polygon(makeLinearRings(coordinates)) 136 | default: 137 | panic(&InvalidGeometryError{}) 138 | } 139 | case "MultiPolygon": 140 | coordinates := decodeCoordinates4(g.Coordinates) 141 | if len(coordinates) == 0 || len(coordinates[0]) == 0 || len(coordinates[0][0]) == 0 { 142 | panic(&InvalidGeometryError{}) 143 | } 144 | switch len(coordinates[0][0][0]) { 145 | case 2: 146 | multiPolygon := make(geom.MultiPolygon, len(coordinates)) 147 | for i, coord := range coordinates { 148 | multiPolygon[i] = makeLinearRings(coord) 149 | } 150 | return multiPolygon 151 | default: 152 | panic(&InvalidGeometryError{}) 153 | } 154 | default: 155 | panic(&UnsupportedGeometryError{g.Type}) 156 | } 157 | } 158 | 159 | func FromGeoJSON(geom *Geometry) (g geom.Geom, err error) { 160 | defer func() { 161 | if e := recover(); e != nil { 162 | g = nil 163 | err = e.(error) 164 | } 165 | }() 166 | return doFromGeoJSON(geom), nil 167 | } 168 | 169 | func Decode(data []byte) (geom.Geom, error) { 170 | var geom Geometry 171 | if err := json.Unmarshal(data, &geom); err == nil { 172 | return FromGeoJSON(&geom) 173 | } else { 174 | return nil, err 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /bounds.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import ( 4 | "math" 5 | ) 6 | 7 | // Bounds holds the spatial extent of a geometry. 8 | type Bounds struct { 9 | Min, Max Point 10 | } 11 | 12 | // Extend increases the extent of b1 to include b2. 13 | func (b *Bounds) Extend(b2 *Bounds) { 14 | if b2 == nil { 15 | return 16 | } 17 | b.extendPoint(b2.Min) 18 | b.extendPoint(b2.Max) 19 | } 20 | 21 | // NewBounds initializes a new bounds object. 22 | func NewBounds() *Bounds { 23 | return &Bounds{Point{X: math.Inf(1), Y: math.Inf(1)}, Point{X: math.Inf(-1), Y: math.Inf(-1)}} 24 | } 25 | 26 | // NewBoundsPoint creates a bounds object from a point. 27 | func NewBoundsPoint(point Point) *Bounds { 28 | return &Bounds{Point{X: point.X, Y: point.Y}, Point{X: point.X, Y: point.Y}} 29 | } 30 | 31 | // Copy returns a copy of b. 32 | func (b *Bounds) Copy() *Bounds { 33 | return &Bounds{Point{X: b.Min.X, Y: b.Min.Y}, Point{X: b.Max.X, Y: b.Max.Y}} 34 | } 35 | 36 | // Empty returns true if b does not contain any points. 37 | func (b *Bounds) Empty() bool { 38 | return b.Max.X < b.Min.X || b.Max.Y < b.Min.Y 39 | } 40 | 41 | func (b *Bounds) extendPoint(point Point) *Bounds { 42 | b.Min.X = math.Min(b.Min.X, point.X) 43 | b.Min.Y = math.Min(b.Min.Y, point.Y) 44 | b.Max.X = math.Max(b.Max.X, point.X) 45 | b.Max.Y = math.Max(b.Max.Y, point.Y) 46 | return b 47 | } 48 | 49 | func (b *Bounds) extendPoints(points []Point) { 50 | for _, point := range points { 51 | b.extendPoint(point) 52 | } 53 | } 54 | 55 | func (b *Bounds) extendPointss(pointss []Path) { 56 | for _, points := range pointss { 57 | b.extendPoints(points) 58 | } 59 | } 60 | 61 | // Overlaps returns whether b and b2 overlap. 62 | func (b *Bounds) Overlaps(b2 *Bounds) bool { 63 | return b.Min.X <= b2.Max.X && b.Min.Y <= b2.Max.Y && b.Max.X >= b2.Min.X && b.Max.Y >= b2.Min.Y 64 | } 65 | 66 | // Bounds returns b 67 | func (b *Bounds) Bounds() *Bounds { 68 | return b 69 | } 70 | 71 | // Within calculates whether b is within poly. 72 | func (b *Bounds) Within(poly Polygonal) WithinStatus { 73 | if bp, ok := poly.(*Bounds); ok { 74 | if b.Min.Equals(bp.Min) && b.Max.Equals(bp.Max) { 75 | return OnEdge 76 | } else if b.Min.X >= bp.Min.X && b.Min.Y >= bp.Min.Y && b.Max.X <= bp.Max.X && b.Max.Y <= bp.Max.Y { 77 | return Inside 78 | } 79 | return Outside 80 | } 81 | minIn := pointInPolygonal(b.Min, poly) 82 | maxIn := pointInPolygonal(b.Max, poly) 83 | if minIn == Outside || maxIn == Outside { 84 | return Outside 85 | } 86 | return Inside 87 | } 88 | 89 | // Len returns the number of points in the receiver (always==5). 90 | func (b *Bounds) Len() int { return 4 } 91 | 92 | // Points returns an iterator for the corners of the receiver. 93 | func (b *Bounds) Points() func() Point { 94 | var i int 95 | return func() Point { 96 | defer func() { 97 | i++ 98 | }() 99 | switch i { 100 | case 0: 101 | return b.Min 102 | case 1: 103 | return Point{b.Max.X, b.Min.Y} 104 | case 2: 105 | return b.Max 106 | case 3: 107 | return Point{b.Min.X, b.Max.Y} 108 | default: 109 | panic("out of bounds") 110 | } 111 | } 112 | } 113 | 114 | // Polygons returns a rectangle polygon 115 | // to fulfill the Polygonal interface. 116 | func (b *Bounds) Polygons() []Polygon { 117 | return []Polygon{{{b.Min, Point{b.Max.X, b.Min.Y}, b.Max, Point{b.Min.X, b.Max.Y}}}} 118 | } 119 | 120 | // Intersection returns the Intersection of the receiver with p. 121 | func (b *Bounds) Intersection(p Polygonal) Polygonal { 122 | if bp, ok := p.(*Bounds); ok { 123 | // Special case, other polygon is *Bounds. 124 | i := &Bounds{ 125 | Min: Point{X: math.Max(b.Min.X, bp.Min.X), Y: math.Max(b.Min.Y, bp.Min.Y)}, 126 | Max: Point{X: math.Min(b.Max.X, bp.Max.X), Y: math.Min(b.Max.Y, bp.Max.Y)}, 127 | } 128 | if i.Min.X >= i.Max.X && i.Min.Y >= i.Max.Y { 129 | return nil 130 | } 131 | return i 132 | } 133 | 134 | bp := p.Bounds() 135 | if w := bp.Within(b); w == Inside || w == OnEdge { 136 | // Polygon fully within bounds. 137 | return p 138 | } else if bbp, ok := p.(*Bounds); ok { 139 | // Polygon is bounds. 140 | if w := b.Within(bbp); w == Inside || w == OnEdge { 141 | return b 142 | } 143 | } 144 | if !b.Overlaps(bp) { 145 | return nil 146 | } 147 | return b.Polygons()[0].Intersection(p) 148 | } 149 | 150 | // Union returns the combination of the receiver and p. 151 | func (b *Bounds) Union(p Polygonal) Polygonal { 152 | // TODO: optimize 153 | return b.Polygons()[0].Union(p) 154 | } 155 | 156 | // XOr returns the area(s) occupied by either the receiver or p but not both. 157 | func (b *Bounds) XOr(p Polygonal) Polygonal { 158 | // TODO: optimize 159 | return b.Polygons()[0].XOr(p) 160 | } 161 | 162 | // Difference subtracts p from b. 163 | func (b *Bounds) Difference(p Polygonal) Polygonal { 164 | // TODO: optimize 165 | return b.Polygons()[0].Difference(p) 166 | } 167 | 168 | // Area returns the area of the reciever. 169 | func (b *Bounds) Area() float64 { 170 | return (b.Max.X - b.Min.X) * (b.Max.Y - b.Min.Y) 171 | } 172 | 173 | // Simplify returns the receiver 174 | // to fulfill the Polygonal interface. 175 | func (b *Bounds) Simplify(tolerance float64) Geom { 176 | return b 177 | } 178 | 179 | func (b *Bounds) Centroid() Point { 180 | return Point{(b.Min.X + b.Max.X) / 2, (b.Min.Y + b.Max.Y) / 2} 181 | } 182 | -------------------------------------------------------------------------------- /index/rtree/README.md: -------------------------------------------------------------------------------- 1 | rtreego 2 | ======= 3 | 4 | A library for efficiently storing and querying spatial data 5 | in the Go programming language. 6 | 7 | Forked from github.com/dhconnelly/rtreego to specialize for 3 dimensions 8 | and tune for fewer memory allocations. 9 | 10 | 11 | About 12 | ----- 13 | 14 | The R-tree is a popular data structure for efficiently storing and 15 | querying spatial objects; one common use is implementing geospatial 16 | indexes in database management systems. The variant implemented here, 17 | known as the R*-tree, improves performance and increases storage 18 | utilization. Both bounding-box queries and k-nearest-neighbor queries 19 | are supported. 20 | 21 | R-trees are balanced, so maximum tree height is guaranteed to be 22 | logarithmic in the number of entries; however, good worst-case 23 | performance is not guaranteed. Instead, a number of rebalancing 24 | heuristics are applied that perform well in practice. For more 25 | details please refer to the references. 26 | 27 | 28 | Status 29 | ------ 30 | 31 | Geometric primitives (points, rectangles, and their relevant geometric 32 | algorithms) are implemented and tested. The R-tree data structure and 33 | algorithms are currently under development. 34 | 35 | 36 | Install 37 | ------- 38 | 39 | With Go 1 installed, just run `go get github.com/patrick-higgins/rtreego`. 40 | 41 | 42 | Usage 43 | ----- 44 | 45 | Make sure you `import github.com/patrick-higgins/rtreego` in your Go source files. 46 | 47 | ### Storing, updating, and deleting objects 48 | 49 | To create a new tree, specify the number of spatial dimensions and the minimum 50 | and maximum branching factor: 51 | 52 | rt := rtreego.NewTree(2, 25, 50) 53 | 54 | Any type that implements the `Spatial` interface can be stored in the tree: 55 | 56 | type Spatial interface { 57 | Bounds() *Rect 58 | } 59 | 60 | `Rect`s are data structures for representing spatial objects, while `Point`s 61 | represent spatial locations. Creating `Point`s is easy--they're just slices 62 | of `float64`s: 63 | 64 | p1 := rtreego.Point{0.4, 0.5} 65 | p2 := rtreego.Point{6.2, -3.4} 66 | 67 | To create a `Rect`, specify a location and the lengths of the sides: 68 | 69 | r1 := rtreego.NewRect(p1, []float64{1, 2}) 70 | r2 := rtreego.NewRect(p2, []float64{1.7, 2.7}) 71 | 72 | To demonstrate, let's create and store some test data. 73 | 74 | type Thing struct { 75 | where *Rect 76 | name string 77 | } 78 | 79 | func (t *Thing) Bounds() *Rect { 80 | return t.where 81 | } 82 | 83 | rt.Insert(&Thing{r1, "foo"}) 84 | rt.Insert(&Thing{r2, "bar"}) 85 | 86 | size := rt.Size() // returns 2 87 | 88 | We can insert and delete objects from the tree in any order. 89 | 90 | rt.Delete(thing2) 91 | // do some stuff... 92 | rt.Insert(anotherThing) 93 | 94 | If you want to store points instead of rectangles, you can easily convert a 95 | point into a rectangle using the `ToRect` method: 96 | 97 | var tol = 0.01 98 | 99 | type Somewhere struct { 100 | location rtreego.Point 101 | name string 102 | wormhole chan int 103 | } 104 | 105 | func (s *Somewhere) Bounds() *Rect { 106 | // define the bounds of s to be a rectangle centered at s.location 107 | // with side lengths 2 * tol: 108 | return s.location.ToRect(tol) 109 | } 110 | 111 | rt.Insert(&Somewhere{rtreego.Point{0, 0}, "Someplace", nil}) 112 | 113 | If you want to update the location of an object, you must delete it, update it, 114 | and re-insert. Just modifying the object so that the `*Rect` returned by 115 | `Location()` changes, without deleting and re-inserting the object, will 116 | corrupt the tree. 117 | 118 | ### Queries 119 | 120 | Bounding-box and k-nearest-neighbors queries are supported. 121 | 122 | Bounding-box queries require a search `*Rect` argument and come in two flavors: 123 | containment search and intersection search. The former returns all objects that 124 | fall strictly inside the search rectangle, while the latter returns all objects 125 | that touch the search rectangle. 126 | 127 | bb := rtreego.NewRect(rtreego.Point{1.7, -3.4}, []float64{3.2, 1.9}) 128 | 129 | // Get a slice of the objects in rt that intersect bb: 130 | results, _ := rt.SearchIntersect(bb) 131 | 132 | // Get a slice of the objects in rt that are contained inside bb: 133 | results, _ = rt.SearchContained(bb) 134 | 135 | Nearest-neighbor queries find the objects in a tree closest to a specified 136 | query point. 137 | 138 | q := rtreego.Point{6.5, -2.47} 139 | k := 5 140 | 141 | // Get a slice of the k objects in rt closest to q: 142 | results, _ = rt.SearchNearestNeighbors(q, k) 143 | 144 | ### More information 145 | 146 | See http://github.com/patrick-higgins/rtreego for full API documentation. 147 | 148 | 149 | References 150 | ---------- 151 | 152 | - A. Guttman. R-trees: A Dynamic Index Structure for Spatial Searching. 153 | Proceedings of ACM SIGMOD, pages 47-57, 1984. 154 | http://www.cs.jhu.edu/~misha/ReadingSeminar/Papers/Guttman84.pdf 155 | 156 | - N. Beckmann, H .P. Kriegel, R. Schneider and B. Seeger. The R*-tree: An 157 | Efficient and Robust Access Method for Points and Rectangles. Proceedings 158 | of ACM SIGMOD, pages 323-331, May 1990. 159 | http://infolab.usc.edu/csci587/Fall2011/papers/p322-beckmann.pdf 160 | 161 | - N. Roussopoulos, S. Kelley and F. Vincent. Nearest Neighbor Queries. ACM 162 | SIGMOD, pages 71-79, 1995. 163 | http://www.postgis.org/support/nearestneighbor.pdf 164 | 165 | 166 | Author 167 | ------ 168 | 169 | rtreego is written and maintained by Daniel Connelly. You can find my stuff 170 | at dhconnelly.com or email me at dhconnelly@gmail.com. 171 | 172 | This fork is maintained by Patrick Higgins (patrick.allen.higgins@gmail.com). 173 | 174 | 175 | License 176 | ------- 177 | 178 | rtreego is released under a BSD-style license; see LICENSE for more details. 179 | -------------------------------------------------------------------------------- /encoding/osm/tags.go: -------------------------------------------------------------------------------- 1 | package osm 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io" 7 | "runtime" 8 | "sort" 9 | 10 | "github.com/paulmach/osm" 11 | "github.com/paulmach/osm/osmpbf" 12 | ) 13 | 14 | // Tags holds information about the tags that are in a database. 15 | type Tags []*TagCount 16 | 17 | // Len returns the length of the receiver to implement the sort.Sort interface. 18 | func (t *Tags) Len() int { return len(*t) } 19 | 20 | // Less returns whether item i is less than item j 21 | // to implement the sort.Sort interface. 22 | func (t *Tags) Less(i, j int) bool { 23 | tt := *t 24 | if tt[i].TotalCount < tt[j].TotalCount { 25 | return true 26 | } 27 | if tt[i].TotalCount > tt[j].TotalCount { 28 | return false 29 | } 30 | if tt[i].Key < tt[j].Key { 31 | return true 32 | } 33 | if tt[i].Key > tt[j].Key { 34 | return false 35 | } 36 | return tt[i].Value < tt[j].Value 37 | } 38 | 39 | // Table creates a table of the information held by the receiver. 40 | func (t *Tags) Table() [][]string { 41 | o := make([][]string, len(*t)+1) 42 | o[0] = []string{"Key", "Value", "Total", "Node", "Closed way", "Open way", "Relation"} 43 | for i, tt := range *t { 44 | o[i+1] = []string{ 45 | tt.Key, 46 | tt.Value, 47 | fmt.Sprintf("%d", tt.TotalCount), 48 | fmt.Sprintf("%d", tt.ObjectCount[NodeType]), 49 | fmt.Sprintf("%d", tt.ObjectCount[ClosedWayType]), 50 | fmt.Sprintf("%d", tt.ObjectCount[OpenWayType]), 51 | fmt.Sprintf("%d", tt.ObjectCount[RelationType]), 52 | } 53 | } 54 | return o 55 | } 56 | 57 | // Filter applies function f to all records in the receiver 58 | // and returns a copy of the receiver that only contains 59 | // the records for which f returns true. 60 | func (t *Tags) Filter(f func(*TagCount) bool) *Tags { 61 | var o Tags 62 | for _, tt := range *t { 63 | if f(tt) { 64 | o = append(o, tt) 65 | } 66 | } 67 | return &o 68 | } 69 | 70 | // Swap swaps elements i and j 71 | // to implement the sort.Sort interface. 72 | func (t *Tags) Swap(i, j int) { (*t)[i], (*t)[j] = (*t)[j], (*t)[i] } 73 | 74 | // TagCount hold information about the number of instances of 75 | // the specified tag in a database. 76 | type TagCount struct { 77 | Key, Value string 78 | ObjectCount map[ObjectType]int 79 | TotalCount int 80 | } 81 | 82 | // DominantType returns the most frequently occuring ObjectType for 83 | // this tag. 84 | func (t *TagCount) DominantType() ObjectType { 85 | v := 0 86 | result := ObjectType(-1) 87 | for typ, vv := range t.ObjectCount { 88 | if vv > v { 89 | result = typ 90 | v = vv 91 | } 92 | } 93 | return result 94 | } 95 | 96 | // CountTags returns the different tags in the database and the number of 97 | // instances of each one. 98 | func CountTags(ctx context.Context, rs io.ReadSeeker) (Tags, error) { 99 | tags := make(map[string]map[string]*TagCount) 100 | 101 | addTag := func(key, val string, typ ObjectType) { 102 | if _, ok := tags[key]; !ok { 103 | tags[key] = make(map[string]*TagCount) 104 | } 105 | if _, ok := tags[key][val]; !ok { 106 | tags[key][val] = &TagCount{ 107 | Key: key, 108 | Value: val, 109 | ObjectCount: make(map[ObjectType]int), 110 | } 111 | } 112 | t := tags[key][val] 113 | t.ObjectCount[typ]++ 114 | t.TotalCount++ 115 | tags[key][val] = t 116 | } 117 | 118 | if _, err := rs.Seek(0, 0); err != nil { 119 | return nil, err 120 | } 121 | scanner := osmpbf.New(ctx, rs, runtime.GOMAXPROCS(-1)) 122 | for scanner.Scan() { 123 | obj := scanner.Object() 124 | switch vtype := obj.(type) { 125 | case *osm.Node: 126 | for _, t := range obj.(*osm.Node).Tags { 127 | addTag(t.Key, t.Value, NodeType) 128 | } 129 | case *osm.Way: 130 | if w := obj.(*osm.Way); wayIsClosed(copyWay(w, true)) { 131 | for _, t := range w.Tags { 132 | addTag(t.Key, t.Value, ClosedWayType) 133 | } 134 | } else { 135 | for _, t := range w.Tags { 136 | addTag(t.Key, t.Value, OpenWayType) 137 | } 138 | } 139 | case *osm.Relation: 140 | for _, t := range obj.(*osm.Relation).Tags { 141 | addTag(t.Key, t.Value, RelationType) 142 | } 143 | default: 144 | return nil, fmt.Errorf("unknown type %T", vtype) 145 | } 146 | } 147 | if err := scanner.Err(); err != nil { 148 | return nil, err 149 | } 150 | if err := scanner.Close(); err != nil { 151 | return nil, err 152 | } 153 | 154 | var tagList Tags 155 | for _, d := range tags { 156 | for _, d2 := range d { 157 | tagList = append(tagList, d2) 158 | } 159 | } 160 | sort.Sort(sort.Reverse(&tagList)) 161 | return tagList, nil 162 | } 163 | 164 | // CountTags returns the different tags in the receiver and the number of 165 | // instances of each one. 166 | func (o *Data) CountTags() Tags { 167 | tags := make(map[string]map[string]*TagCount) 168 | 169 | addTag := func(key, val string, typ ObjectType) { 170 | if _, ok := tags[key]; !ok { 171 | tags[key] = make(map[string]*TagCount) 172 | } 173 | if _, ok := tags[key][val]; !ok { 174 | tags[key][val] = &TagCount{ 175 | Key: key, 176 | Value: val, 177 | ObjectCount: make(map[ObjectType]int), 178 | } 179 | } 180 | t := tags[key][val] 181 | t.ObjectCount[typ]++ 182 | t.TotalCount++ 183 | tags[key][val] = t 184 | } 185 | for _, n := range o.Nodes { 186 | for _, t := range n.Tags { 187 | addTag(t.Key, t.Value, NodeType) 188 | } 189 | } 190 | for _, w := range o.Ways { 191 | if wayIsClosed(w) { 192 | for _, t := range w.Tags { 193 | addTag(t.Key, t.Value, ClosedWayType) 194 | } 195 | } else { 196 | for _, t := range w.Tags { 197 | addTag(t.Key, t.Value, OpenWayType) 198 | } 199 | } 200 | } 201 | for _, r := range o.Relations { 202 | for _, t := range r.Tags { 203 | addTag(t.Key, t.Value, RelationType) 204 | } 205 | } 206 | 207 | var tagList Tags 208 | for _, d := range tags { 209 | for _, d2 := range d { 210 | tagList = append(tagList, d2) 211 | } 212 | } 213 | sort.Sort(sort.Reverse(&tagList)) 214 | return tagList 215 | } 216 | -------------------------------------------------------------------------------- /proj/EllipsoidDef.go: -------------------------------------------------------------------------------- 1 | package proj 2 | 3 | type ellipsoidDef struct { 4 | a, b, rf float64 5 | ellipseName string 6 | } 7 | 8 | var ellipsoidDefs = map[string]ellipsoidDef{ 9 | "MERIT": ellipsoidDef{ 10 | a: 6378137.0, 11 | rf: 298.257, 12 | ellipseName: "MERIT 1983", 13 | }, 14 | "SGS85": ellipsoidDef{ 15 | a: 6378136.0, 16 | rf: 298.257, 17 | ellipseName: "Soviet Geodetic System 85", 18 | }, 19 | "GRS80": ellipsoidDef{ 20 | a: 6378137.0, 21 | rf: 298.257222101, 22 | ellipseName: "GRS 1980(IUGG, 1980)", 23 | }, 24 | "IAU76": ellipsoidDef{ 25 | a: 6378140.0, 26 | rf: 298.257, 27 | ellipseName: "IAU 1976", 28 | }, 29 | "airy": ellipsoidDef{ 30 | a: 6377563.396, 31 | b: 6356256.910, 32 | ellipseName: "Airy 1830", 33 | }, 34 | "APL4": ellipsoidDef{ 35 | a: 6378137, 36 | rf: 298.25, 37 | ellipseName: "Appl. Physics. 1965", 38 | }, 39 | "NWL9D": ellipsoidDef{ 40 | a: 6378145.0, 41 | rf: 298.25, 42 | ellipseName: "Naval Weapons Lab., 1965", 43 | }, 44 | "mod_airy": ellipsoidDef{ 45 | a: 6377340.189, 46 | b: 6356034.446, 47 | ellipseName: "Modified Airy", 48 | }, 49 | "andrae": ellipsoidDef{ 50 | a: 6377104.43, 51 | rf: 300.0, 52 | ellipseName: "Andrae 1876 (Den., Iclnd.)", 53 | }, 54 | "aust_SA": ellipsoidDef{ 55 | a: 6378160.0, 56 | rf: 298.25, 57 | ellipseName: "Australian Natl & S. Amer. 1969", 58 | }, 59 | "GRS67": ellipsoidDef{ 60 | a: 6378160.0, 61 | rf: 298.2471674270, 62 | ellipseName: "GRS 67(IUGG 1967)", 63 | }, 64 | "bessel": ellipsoidDef{ 65 | a: 6377397.155, 66 | rf: 299.1528128, 67 | ellipseName: "Bessel 1841", 68 | }, 69 | "bess_nam": ellipsoidDef{ 70 | a: 6377483.865, 71 | rf: 299.1528128, 72 | ellipseName: "Bessel 1841 (Namibia)", 73 | }, 74 | "clrk66": ellipsoidDef{ 75 | a: 6378206.4, 76 | b: 6356583.8, 77 | ellipseName: "Clarke 1866", 78 | }, 79 | "clrk80": ellipsoidDef{ 80 | a: 6378249.145, 81 | rf: 293.4663, 82 | ellipseName: "Clarke 1880 mod.", 83 | }, 84 | "clrk58": ellipsoidDef{ 85 | a: 6378293.645208759, 86 | rf: 294.2606763692654, 87 | ellipseName: "Clarke 1858", 88 | }, 89 | "CPM": ellipsoidDef{ 90 | a: 6375738.7, 91 | rf: 334.29, 92 | ellipseName: "Comm. des Poids et Mesures 1799", 93 | }, 94 | "delmbr": ellipsoidDef{ 95 | a: 6376428.0, 96 | rf: 311.5, 97 | ellipseName: "Delambre 1810 (Belgium)", 98 | }, 99 | "engelis": ellipsoidDef{ 100 | a: 6378136.05, 101 | rf: 298.2566, 102 | ellipseName: "Engelis 1985", 103 | }, 104 | "evrst30": ellipsoidDef{ 105 | a: 6377276.345, 106 | rf: 300.8017, 107 | ellipseName: "Everest 1830", 108 | }, 109 | "evrst48": ellipsoidDef{ 110 | a: 6377304.063, 111 | rf: 300.8017, 112 | ellipseName: "Everest 1948", 113 | }, 114 | "evrst56": ellipsoidDef{ 115 | a: 6377301.243, 116 | rf: 300.8017, 117 | ellipseName: "Everest 1956", 118 | }, 119 | "evrst69": ellipsoidDef{ 120 | a: 6377295.664, 121 | rf: 300.8017, 122 | ellipseName: "Everest 1969", 123 | }, 124 | "evrstSS": ellipsoidDef{ 125 | a: 6377298.556, 126 | rf: 300.8017, 127 | ellipseName: "Everest (Sabah & Sarawak)", 128 | }, 129 | "fschr60": ellipsoidDef{ 130 | a: 6378166.0, 131 | rf: 298.3, 132 | ellipseName: "Fischer (Mercury Datum) 1960", 133 | }, 134 | "fschr60m": ellipsoidDef{ 135 | a: 6378155.0, 136 | rf: 298.3, 137 | ellipseName: "Fischer 1960", 138 | }, 139 | "fschr68": ellipsoidDef{ 140 | a: 6378150.0, 141 | rf: 298.3, 142 | ellipseName: "Fischer 1968", 143 | }, 144 | "helmert": ellipsoidDef{ 145 | a: 6378200.0, 146 | rf: 298.3, 147 | ellipseName: "Helmert 1906", 148 | }, 149 | "hough": ellipsoidDef{ 150 | a: 6378270.0, 151 | rf: 297.0, 152 | ellipseName: "Hough", 153 | }, 154 | "intl": ellipsoidDef{ 155 | a: 6378388.0, 156 | rf: 297.0, 157 | ellipseName: "International 1909 (Hayford)", 158 | }, 159 | "kaula": ellipsoidDef{ 160 | a: 6378163.0, 161 | rf: 298.24, 162 | ellipseName: "Kaula 1961", 163 | }, 164 | "lerch": ellipsoidDef{ 165 | a: 6378139.0, 166 | rf: 298.257, 167 | ellipseName: "Lerch 1979", 168 | }, 169 | "mprts": ellipsoidDef{ 170 | a: 6397300.0, 171 | rf: 191.0, 172 | ellipseName: "Maupertius 1738", 173 | }, 174 | "new_intl": ellipsoidDef{ 175 | a: 6378157.5, 176 | b: 6356772.2, 177 | ellipseName: "New International 1967", 178 | }, 179 | "plessis": ellipsoidDef{ 180 | a: 6376523.0, 181 | rf: 6355863.0, 182 | ellipseName: "Plessis 1817 (France)", 183 | }, 184 | "krass": ellipsoidDef{ 185 | a: 6378245.0, 186 | rf: 298.3, 187 | ellipseName: "Krassovsky, 1942", 188 | }, 189 | "SEasia": ellipsoidDef{ 190 | a: 6378155.0, 191 | b: 6356773.3205, 192 | ellipseName: "Southeast Asia", 193 | }, 194 | "walbeck": ellipsoidDef{ 195 | a: 6376896.0, 196 | b: 6355834.8467, 197 | ellipseName: "Walbeck", 198 | }, 199 | "WGS60": ellipsoidDef{ 200 | a: 6378165.0, 201 | rf: 298.3, 202 | ellipseName: "WGS 60", 203 | }, 204 | "WGS66": ellipsoidDef{ 205 | a: 6378145.0, 206 | rf: 298.25, 207 | ellipseName: "WGS 66", 208 | }, 209 | "WGS7": ellipsoidDef{ 210 | a: 6378135.0, 211 | rf: 298.26, 212 | ellipseName: "WGS 72", 213 | }, 214 | "WGS84": ellipsoidDef{ 215 | a: 6378137.0, 216 | rf: 298.257223563, 217 | ellipseName: "WGS 84", 218 | }, 219 | "sphere": ellipsoidDef{ 220 | a: 6370997.0, 221 | b: 6370997.0, 222 | ellipseName: "Normal Sphere (r=6370997)", 223 | }, 224 | } 225 | -------------------------------------------------------------------------------- /simplify.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "math" 4 | 5 | // Simplifier is an interface for types that can be simplified. 6 | type Simplifier interface { 7 | Simplify(tolerance float64) Geom 8 | } 9 | 10 | // Simplify simplifies p 11 | // by removing points according to the tolerance parameter, 12 | // while ensuring that the resulting shape is not self intersecting 13 | // (but only if the input shape is not self intersecting). Self-intersecting 14 | // polygons may cause the algorithm to fall into an infinite loop. 15 | // 16 | // It is based on the algorithm: 17 | // J. L. G. Pallero, Robust line simplification on the plane. 18 | // Comput. Geosci. 61, 152–159 (2013). 19 | func (p Polygon) Simplify(tolerance float64) Geom { 20 | var out Polygon = make([]Path, len(p)) 21 | for i, r := range p { 22 | out[i] = simplifyCurve(r, p, tolerance) 23 | } 24 | return out 25 | } 26 | 27 | // Simplify simplifies mp 28 | // by removing points according to the tolerance parameter, 29 | // while ensuring that the resulting shape is not self intersecting 30 | // (but only if the input shape is not self intersecting). Self-intersecting 31 | // polygons may cause the algorithm to fall into an infinite loop. 32 | // 33 | // It is based on the algorithm: 34 | // J. L. G. Pallero, Robust line simplification on the plane. 35 | // Comput. Geosci. 61, 152–159 (2013). 36 | func (mp MultiPolygon) Simplify(tolerance float64) Geom { 37 | out := make(MultiPolygon, len(mp)) 38 | for i, p := range mp { 39 | out[i] = p.Simplify(tolerance).(Polygon) 40 | } 41 | return out 42 | } 43 | 44 | // Simplify simplifies l 45 | // by removing points according to the tolerance parameter, 46 | // while ensuring that the resulting shape is not self intersecting 47 | // (but only if the input shape is not self intersecting). 48 | // 49 | // It is based on the algorithm: 50 | // J. L. G. Pallero, Robust line simplification on the plane. 51 | // Comput. Geosci. 61, 152–159 (2013). 52 | func (l LineString) Simplify(tolerance float64) Geom { 53 | return LineString(simplifyCurve(Path(l), []Path{}, tolerance)) 54 | } 55 | 56 | // Simplify simplifies ml 57 | // by removing points according to the tolerance parameter, 58 | // while ensuring that the resulting shape is not self intersecting 59 | // (but only if the input shape is not self intersecting). 60 | // 61 | // It is based on the algorithm: 62 | // J. L. G. Pallero, Robust line simplification on the plane. 63 | // Comput. Geosci. 61, 152–159 (2013). 64 | func (ml MultiLineString) Simplify(tolerance float64) Geom { 65 | out := make(MultiLineString, len(ml)) 66 | for i, l := range ml { 67 | out[i] = l.Simplify(tolerance).(LineString) 68 | } 69 | return out 70 | } 71 | 72 | func simplifyCurve(curve Path, 73 | otherCurves []Path, tol float64) []Point { 74 | out := make([]Point, 0, len(curve)) 75 | 76 | if len(curve) == 0 { 77 | return nil 78 | } 79 | 80 | i := 0 81 | for { 82 | out = append(out, curve[i]) 83 | breakTime := false 84 | for j := i + 2; j < len(curve); j++ { 85 | breakTime2 := false 86 | for k := i + 1; k < j; k++ { 87 | d := distPointToSegment(curve[k], curve[i], curve[j]) 88 | if d > tol { 89 | // we have found a candidate point to keep 90 | for { 91 | // Make sure this simplification doesn't cause any self 92 | // intersections. 93 | if j > i+2 && 94 | (segMakesNotSimple(curve[i], curve[j-1], []Path{out[0:i]}) || 95 | segMakesNotSimple(curve[i], curve[j-1], []Path{curve[j:]}) || 96 | segMakesNotSimple(curve[i], curve[j-1], otherCurves)) { 97 | j-- 98 | } else { 99 | i = j - 1 100 | out = append(out, curve[i]) 101 | breakTime2 = true 102 | break 103 | } 104 | } 105 | } 106 | if breakTime2 { 107 | break 108 | } 109 | } 110 | if j == len(curve)-1 { 111 | // Add last point regardless of distance. 112 | out = append(out, curve[j]) 113 | breakTime = true 114 | } 115 | } 116 | if breakTime { 117 | break 118 | } 119 | } 120 | return out 121 | } 122 | 123 | func segMakesNotSimple(segStart, segEnd Point, paths []Path) bool { 124 | seg1 := segment{segStart, segEnd} 125 | for _, p := range paths { 126 | for i := 0; i < len(p)-1; i++ { 127 | seg2 := segment{p[i], p[i+1]} 128 | if seg1.start == seg2.start || seg1.end == seg2.end || 129 | seg1.start == seg2.end || seg1.end == seg2.start { 130 | // colocated endpoints are not a problem here 131 | return false 132 | } 133 | numIntersections, _, _ := findIntersection(seg1, seg2) 134 | if numIntersections > 0 { 135 | return true 136 | } 137 | } 138 | } 139 | return false 140 | } 141 | 142 | // pointOnSegment calculates whether point p is exactly on the finite line segment 143 | // defined by points l1 and l2. 144 | func pointOnSegment(p, l1, l2 Point) bool { 145 | if (p.X < l1.X && p.X < l2.X) || (p.X > l1.X && p.X > l2.X) || 146 | (p.Y < l1.Y && p.Y < l2.Y) || (p.Y > l1.Y && p.Y > l2.Y) { 147 | return false 148 | } 149 | d1 := pointSubtract(l1, p) 150 | d2 := pointSubtract(l2, l1) 151 | 152 | // If the two slopes are the same, then the point is on the line 153 | if (d1.X == 0 && d2.X == 0) || d1.Y/d1.X == d2.Y/d2.X { 154 | return true 155 | } 156 | return false 157 | } 158 | 159 | // dist_Point_to_Segment(): get the distance of a point to a segment 160 | // Input: a Point P and a Segment S (in any dimension) 161 | // Return: the shortest distance from P to S 162 | // from http://geomalgorithms.com/a02-_lines.html 163 | func distPointToSegment(p, segStart, segEnd Point) float64 { 164 | v := pointSubtract(segEnd, segStart) 165 | w := pointSubtract(p, segStart) 166 | 167 | c1 := dot(w, v) 168 | if c1 <= 0. { 169 | return d(p, segStart) 170 | } 171 | 172 | c2 := dot(v, v) 173 | if c2 <= c1 { 174 | return d(p, segEnd) 175 | } 176 | 177 | b := c1 / c2 178 | pb := Point{segStart.X + b*v.X, segStart.Y + b*v.Y} 179 | return d(p, pb) 180 | } 181 | 182 | func pointSubtract(p1, p2 Point) Point { 183 | return Point{X: p1.X - p2.X, Y: p1.Y - p2.Y} 184 | } 185 | 186 | // dot product 187 | func dot(u, v Point) float64 { return u.X*v.X + u.Y*v.Y } 188 | 189 | // norm = length of vector 190 | func norm(v Point) float64 { return math.Sqrt(dot(v, v)) } 191 | 192 | // distance = norm of difference 193 | func d(u, v Point) float64 { return norm(pointSubtract(u, v)) } 194 | -------------------------------------------------------------------------------- /op/geom.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2011 Mateusz Czapliński (Go port) 2 | // Copyright (c) 2011 Mahir Iqbal (as3 version) 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | // 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | // based on http://code.google.com/p/as3polyclip/ (MIT licensed) 22 | // and code by Martínez et al: http://wwwdi.ujaen.es/~fmartin/bool_op.html (public domain) 23 | 24 | // Package op provides implementation of algorithms for geometry operations. 25 | // For further details, consult the description of Polygon.Construct method. 26 | package op 27 | 28 | import ( 29 | "fmt" 30 | "math" 31 | "reflect" 32 | 33 | "github.com/ctessum/geom" 34 | ) 35 | 36 | // Equals returns true if both p1 and p2 describe the same point within 37 | // a tolerance limit. 38 | func PointEquals(p1, p2 geom.Point) bool { 39 | //return (p1.X == p2.X && p1.Y == p2.Y) 40 | return (p1.X == p2.X && p1.Y == p2.Y) || 41 | (math.Abs(p1.X-p2.X)/math.Abs(p1.X+p2.X) < tolerance && 42 | math.Abs(p1.Y-p2.Y)/math.Abs(p1.Y+p2.Y) < tolerance) 43 | } 44 | 45 | func floatEquals(f1, f2 float64) bool { 46 | //return (f1 == f2) 47 | return (f1 == f2) || 48 | (math.Abs(f1-f2)/math.Abs(f1+f2) < tolerance) 49 | } 50 | 51 | func pointSubtract(p1, p2 geom.Point) geom.Point { 52 | return geom.Point{p1.X - p2.X, p1.Y - p2.Y} 53 | } 54 | 55 | // Length returns distance from p to point (0, 0). 56 | func lengthToOrigin(p geom.Point) float64 { 57 | return math.Sqrt(p.X*p.X + p.Y*p.Y) 58 | } 59 | 60 | // Used to represent an edge of a polygon. 61 | type segment struct { 62 | start, end geom.Point 63 | } 64 | 65 | // Contour represents a sequence of vertices connected by line segments, forming a closed shape. 66 | type contour []geom.Point 67 | 68 | func (c contour) segment(index int) segment { 69 | if index == len(c)-1 { 70 | return segment{c[len(c)-1], c[0]} 71 | } 72 | return segment{c[index], c[index+1]} 73 | // if out-of-bounds, we expect panic detected by runtime 74 | } 75 | 76 | // Clone returns a copy of a contour. 77 | func (c contour) Clone() contour { 78 | return append([]geom.Point{}, c...) 79 | } 80 | 81 | // numVertices returns total number of all vertices of all contours of a polygon. 82 | func numVertices(p geom.Polygon) int { 83 | num := 0 84 | for _, c := range p { 85 | num += len(c) 86 | } 87 | return num 88 | } 89 | 90 | // Clone returns a duplicate of a polygon. 91 | func Clone(p geom.Polygon) geom.Polygon { 92 | var r geom.Polygon 93 | r = make([]geom.Path, len(p)) 94 | for i, rr := range p { 95 | r[i] = make([]geom.Point, len(rr)) 96 | for j, pp := range p[i] { 97 | r[i][j] = pp 98 | } 99 | } 100 | return r 101 | } 102 | 103 | // Op describes an operation which can be performed on two polygons. 104 | type Op int 105 | 106 | const ( 107 | UNION Op = iota 108 | INTERSECTION 109 | DIFFERENCE 110 | XOR 111 | ) 112 | 113 | // convert input shapes to polygon to make internal processing easier 114 | func convertToPolygon(g geom.Geom) geom.Polygon { 115 | var out geom.Polygon 116 | switch g.(type) { 117 | case geom.Polygon: 118 | out = g.(geom.Polygon) 119 | case geom.MultiPolygon: 120 | out = make([]geom.Path, 0) 121 | for _, p := range g.(geom.MultiPolygon) { 122 | for _, r := range p { 123 | out = append(out, r) 124 | } 125 | } 126 | case geom.LineString: 127 | g2 := g.(geom.LineString) 128 | out = make([]geom.Path, 1) 129 | out[0] = make([]geom.Point, len(g2)) 130 | for j, p := range g2 { 131 | out[0][j] = p 132 | } 133 | case geom.MultiLineString: 134 | g2 := g.(geom.MultiLineString) 135 | out = make([]geom.Path, len(g2)) 136 | for i, ls := range g2 { 137 | out[i] = make([]geom.Point, len(ls)) 138 | for j, p := range ls { 139 | out[i][j] = p 140 | } 141 | } 142 | default: 143 | panic(newUnsupportedGeometryError(g)) 144 | } 145 | // The clipper doesn't work well if a shape is made up of only two points. 146 | // To get around this problem, if there are only 2 points, we add a third 147 | // one a small distance from the second point. 148 | // However, if there is only 1 point, we just delete the shape. 149 | for i, r := range out { 150 | if len(r) == 0 { 151 | continue 152 | } else if len(r) == 1 { 153 | out[i] = make([]geom.Point, 0) 154 | } else if len(r) == 2 { 155 | const delta = 0.00001 156 | newpt := geom.Point{r[1].X + (r[1].X-r[0].X)*delta, 157 | r[1].Y - (r[1].Y-r[0].Y)*delta} 158 | out[i] = append(r, newpt) 159 | } 160 | } 161 | return out 162 | } 163 | 164 | type UnsupportedGeometryError struct { 165 | G geom.Geom 166 | } 167 | 168 | func newUnsupportedGeometryError(g geom.Geom) UnsupportedGeometryError { 169 | return UnsupportedGeometryError{g} 170 | } 171 | 172 | func (e UnsupportedGeometryError) Error() string { 173 | if e.G == nil { 174 | return "Geometry is nil." 175 | } else { 176 | return "Unsupported geometry type: " + reflect.TypeOf(e.G).String() 177 | } 178 | } 179 | 180 | type InfiniteLoopError struct { 181 | s, c geom.Geom 182 | } 183 | 184 | func newInfiniteLoopError(subject, clipping geom.Geom) InfiniteLoopError { 185 | return InfiniteLoopError{s: subject, c: clipping} 186 | } 187 | 188 | func (e InfiniteLoopError) Error() string { 189 | return fmt.Sprintf( 190 | "Function op.Construct appears to have fallen into an "+ 191 | "infinite loop. \n\nSubject geometry=%#v\n\nClipping geometry=%#v", 192 | e.s, e.c) 193 | } 194 | -------------------------------------------------------------------------------- /index/rtree/geom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2012 Daniel Connelly. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package rtree 6 | 7 | import ( 8 | "math" 9 | 10 | "github.com/ctessum/geom" 11 | ) 12 | 13 | // DistError is an improper distance measurement. It implements the error 14 | // and is generated when a distance-related assertion fails. 15 | type DistError geom.Point 16 | 17 | func (err DistError) Error() string { 18 | return "rtreego: improper distance" 19 | } 20 | 21 | // Dist computes the Euclidean distance between two points p and q. 22 | func dist(p, q geom.Point) float64 { 23 | sum := 0.0 24 | dx := p.X - q.X 25 | sum += dx * dx 26 | dx = p.Y - q.Y 27 | sum += dx * dx 28 | return math.Sqrt(sum) 29 | } 30 | 31 | // minDist computes the square of the distance from a point to a rectangle. 32 | // If the point is contained in the rectangle then the distance is zero. 33 | // 34 | // Implemented per Definition 2 of "Nearest Neighbor Queries" by 35 | // N. Roussopoulos, S. Kelley and F. Vincent, ACM SIGMOD, pages 71-79, 1995. 36 | func minDist(p geom.Point, r *geom.Bounds) float64 { 37 | sum := 0.0 38 | if p.X < r.Min.X { 39 | d := p.X - r.Min.X 40 | sum += d * d 41 | } else if p.X > r.Max.X { 42 | d := p.X - r.Max.X 43 | sum += d * d 44 | } else { 45 | sum += 0 46 | } 47 | if p.Y < r.Min.Y { 48 | d := p.Y - r.Min.Y 49 | sum += d * d 50 | } else if p.Y > r.Max.Y { 51 | d := p.Y - r.Max.Y 52 | sum += d * d 53 | } else { 54 | sum += 0 55 | } 56 | return sum 57 | } 58 | 59 | // minMaxDist computes the minimum of the maximum distances from p to points 60 | // on r. If r is the bounding box of some geometric objects, then there is 61 | // at least one object contained in r within minMaxDist(p, r) of p. 62 | // 63 | // Implemented per Definition 4 of "Nearest Neighbor Queries" by 64 | // N. Roussopoulos, S. Kelley and F. Vincent, ACM SIGMOD, pages 71-79, 1995. 65 | func minMaxDist(p geom.Point, r *geom.Bounds) float64 { 66 | // by definition, MinMaxDist(p, r) = 67 | // min{1<=k<=n}(|pk - rmk|^2 + sum{1<=i<=n, i != k}(|pi - rMi|^2)) 68 | // where rmk and rMk are defined as follows: 69 | 70 | rmX := func() float64 { 71 | if p.X <= (r.Min.X+r.Max.X)/2 { 72 | return r.Min.X 73 | } 74 | return r.Max.X 75 | } 76 | rmY := func() float64 { 77 | if p.Y <= (r.Min.Y+r.Max.Y)/2 { 78 | return r.Min.Y 79 | } 80 | return r.Max.Y 81 | } 82 | 83 | rMX := func() float64 { 84 | if p.X >= (r.Min.X+r.Max.X)/2 { 85 | return r.Min.X 86 | } 87 | return r.Max.X 88 | } 89 | rMY := func() float64 { 90 | if p.Y >= (r.Min.Y+r.Max.Y)/2 { 91 | return r.Min.Y 92 | } 93 | return r.Max.Y 94 | } 95 | 96 | // This formula can be computed in linear time by precomputing 97 | // S = sum{1<=i<=n}(|pi - rMi|^2). 98 | 99 | S := 0.0 100 | d := p.X - rMX() 101 | S += d * d 102 | d = p.Y - rMY() 103 | S += d * d 104 | 105 | // Compute MinMaxDist using the precomputed S. 106 | min := math.MaxFloat64 107 | d1 := p.X - rMX() 108 | d2 := p.X - rmX() 109 | d = S - d1*d1 + d2*d2 110 | if d < min { 111 | min = d 112 | } 113 | d1 = p.Y - rMY() 114 | d2 = p.Y - rmY() 115 | d = S - d1*d1 + d2*d2 116 | if d < min { 117 | min = d 118 | } 119 | 120 | return min 121 | } 122 | 123 | // NewRect constructs and returns a pointer to a Rect given a corner point and 124 | // the lengths of each dimension. The point p should be the most-negative point 125 | // on the rectangle (in every dimension) and every length should be positive. 126 | func newRect(p, lengths geom.Point) (r *geom.Bounds, err error) { 127 | r = geom.NewBounds() 128 | r.Min = p 129 | r.Max.X = lengths.X + p.X 130 | r.Max.Y = lengths.Y + p.Y 131 | if lengths.X <= 0 || lengths.Y <= 0 { 132 | return r, DistError(lengths) 133 | } 134 | return r, nil 135 | } 136 | 137 | // size computes the measure of a rectangle (the product of its side lengths). 138 | func size(r *geom.Bounds) float64 { 139 | return (r.Max.X - r.Min.X) * (r.Max.Y - r.Min.Y) 140 | } 141 | 142 | // margin computes the sum of the edge lengths of a rectangle. 143 | func margin(r *geom.Bounds) float64 { 144 | return 2 * ((r.Max.X - r.Min.X) + (r.Max.Y - r.Min.Y)) 145 | } 146 | 147 | // containsPoint tests whether p is located inside or on the boundary of r. 148 | func containsPoint(r *geom.Bounds, p geom.Point) bool { 149 | // p is contained in (or on) r if and only if p <= a <= q for 150 | // every dimension. 151 | if p.X < r.Min.X || p.X > r.Max.X { 152 | return false 153 | } 154 | if p.Y < r.Min.Y || p.Y > r.Max.Y { 155 | return false 156 | } 157 | 158 | return true 159 | } 160 | 161 | // containsRect tests whether r2 is is located inside r1. 162 | func containsRect(r1, r2 *geom.Bounds) bool { 163 | // enforced by constructor: a1 <= b1 and a2 <= b2. 164 | // so containment holds if and only if a1 <= a2 <= b2 <= b1 165 | // for every dimension. 166 | if r1.Min.X > r2.Min.X || r2.Max.X > r1.Max.X { 167 | return false 168 | } 169 | if r1.Min.Y > r2.Min.Y || r2.Max.Y > r1.Max.Y { 170 | return false 171 | } 172 | 173 | return true 174 | } 175 | 176 | func enlarge(r1, r2 *geom.Bounds) { 177 | if r1.Min.X > r2.Min.X { 178 | r1.Min.X = r2.Min.X 179 | } 180 | if r1.Max.X < r2.Max.X { 181 | r1.Max.X = r2.Max.X 182 | } 183 | if r1.Min.Y > r2.Min.Y { 184 | r1.Min.Y = r2.Min.Y 185 | } 186 | if r1.Max.Y < r2.Max.Y { 187 | r1.Max.Y = r2.Max.Y 188 | } 189 | } 190 | 191 | // intersect computes the intersection of two rectangles. If no intersection 192 | // exists, the intersection is nil. 193 | func intersect(r1, r2 *geom.Bounds) bool { 194 | // There are four cases of overlap: 195 | // 196 | // 1. a1------------b1 197 | // a2------------b2 198 | // p--------q 199 | // 200 | // 2. a1------------b1 201 | // a2------------b2 202 | // p--------q 203 | // 204 | // 3. a1-----------------b1 205 | // a2-------b2 206 | // p--------q 207 | // 208 | // 4. a1-------b1 209 | // a2-----------------b2 210 | // p--------q 211 | // 212 | // Thus there are only two cases of non-overlap: 213 | // 214 | // 1. a1------b1 215 | // a2------b2 216 | // 217 | // 2. a1------b1 218 | // a2------b2 219 | // 220 | // Enforced by constructor: a1 <= b1 and a2 <= b2. So we can just 221 | // check the endpoints. 222 | 223 | if r2.Max.X < r1.Min.X || r1.Max.X < r2.Min.X { 224 | return false 225 | } 226 | if r2.Max.Y < r1.Min.Y || r1.Max.Y < r2.Min.Y { 227 | return false 228 | } 229 | return true 230 | } 231 | 232 | // ToRect constructs a rectangle containing p with side lengths 2*tol. 233 | func ToRect(p geom.Point, tol float64) *geom.Bounds { 234 | var r geom.Bounds 235 | r.Min.X = p.X - tol 236 | r.Max.X = p.X + tol 237 | r.Min.Y = p.Y - tol 238 | r.Max.Y = p.Y + tol 239 | return &r 240 | } 241 | 242 | func initBoundingBox(r, r1, r2 *geom.Bounds) { 243 | *r = *r1 244 | enlarge(r, r2) 245 | } 246 | 247 | // boundingBox constructs the smallest rectangle containing both r1 and r2. 248 | func boundingBox(r1, r2 *geom.Bounds) *geom.Bounds { 249 | var r geom.Bounds 250 | initBoundingBox(&r, r1, r2) 251 | return &r 252 | } 253 | -------------------------------------------------------------------------------- /similar.go: -------------------------------------------------------------------------------- 1 | package geom 2 | 3 | import "math" 4 | 5 | func similar(a, b, e float64) bool { 6 | return math.Abs(a-b) < e 7 | } 8 | 9 | func pointSimilar(p1, p2 Point, e float64) bool { 10 | return similar(p1.X, p2.X, e) && similar(p1.Y, p2.Y, e) 11 | } 12 | 13 | func pointsSimilar(p1s, p2s []Point, e float64) bool { 14 | if len(p1s) != len(p2s) { 15 | return false 16 | } 17 | for i, n := 0, len(p1s); i < n; i++ { 18 | if !pointSimilar(p1s[i], p2s[i], e) { 19 | return false 20 | } 21 | } 22 | return true 23 | } 24 | 25 | func pointssSimilar(p1ss, p2ss [][]Point, e float64) bool { 26 | if len(p1ss) != len(p2ss) { 27 | return false 28 | } 29 | for i, n := 0, len(p1ss); i < n; i++ { 30 | if !pointsSimilar(p1ss[i], p2ss[i], e) { 31 | return false 32 | } 33 | } 34 | return true 35 | } 36 | 37 | // Similar determines whether two geometries are similar within tolerance. 38 | func (p Point) Similar(g Geom, tolerance float64) bool { 39 | switch g.(type) { 40 | case Point: 41 | return pointSimilar(p, g.(Point), tolerance) 42 | default: 43 | return false 44 | } 45 | } 46 | 47 | // Similar determines whether two geometries are similar within tolerance. 48 | func (mp MultiPoint) Similar(g Geom, tolerance float64) bool { 49 | switch g.(type) { 50 | case MultiPoint: 51 | return pointsSimilar(mp, g.(MultiPoint), tolerance) 52 | default: 53 | return false 54 | } 55 | } 56 | 57 | // Similar determines whether two geometries are similar within tolerance. 58 | // If two lines contain the same points but in different directions it will 59 | // return false. 60 | func (l LineString) Similar(g Geom, tolerance float64) bool { 61 | switch g.(type) { 62 | case LineString: 63 | return pointsSimilar(l, g.(LineString), tolerance) 64 | default: 65 | return false 66 | } 67 | } 68 | 69 | // Similar determines whether two geometries are similar within tolerance. 70 | // If ml and g have the similar linestrings but in a different order, it 71 | // will return true. 72 | func (ml MultiLineString) Similar(g Geom, tolerance float64) bool { 73 | switch g.(type) { 74 | case MultiLineString: 75 | ml2 := g.(MultiLineString) 76 | indices := make([]int, len(ml2)) 77 | for i := range ml2 { 78 | indices[i] = i 79 | } 80 | for _, l := range ml { 81 | matched := false 82 | for ii, i := range indices { 83 | if l.Similar(ml2[i], tolerance) { // we found a match 84 | matched = true 85 | // remove index i from futher consideration. 86 | if ii == len(indices)-1 { 87 | indices = indices[0:ii] 88 | } else { 89 | indices = append(indices[0:ii], indices[ii+1:len(indices)]...) 90 | } 91 | break 92 | } 93 | } 94 | if !matched { 95 | return false 96 | } 97 | } 98 | return true 99 | default: 100 | return false 101 | } 102 | } 103 | 104 | // Similar determines whether two geometries are similar within tolerance. 105 | // If ml and g have the similar polygons but in a different order, it 106 | // will return true. 107 | func (mp MultiPolygon) Similar(g Geom, tolerance float64) bool { 108 | switch g.(type) { 109 | case MultiPolygon: 110 | mp2 := g.(MultiPolygon) 111 | indices := make([]int, len(mp2)) 112 | for i := range mp2 { 113 | indices[i] = i 114 | } 115 | for _, l := range mp { 116 | matched := false 117 | for ii, i := range indices { 118 | if l.Similar(mp2[i], tolerance) { // we found a match 119 | matched = true 120 | // remove index i from futher consideration. 121 | if ii == len(indices)-1 { 122 | indices = indices[0:ii] 123 | } else { 124 | indices = append(indices[0:ii], indices[ii+1:len(indices)]...) 125 | } 126 | break 127 | } 128 | } 129 | if !matched { 130 | return false 131 | } 132 | } 133 | return true 134 | default: 135 | return false 136 | } 137 | } 138 | 139 | // Similar determines whether two geometries are similar within tolerance. 140 | // If p and g have the same points with the same winding direction, but a 141 | // different starting point, it will return true. If they have the same 142 | // rings but in a different order, it will return true. If the rings have the same 143 | // points but different winding directions, it will return false. 144 | func (p Polygon) Similar(g Geom, tolerance float64) bool { 145 | switch g.(type) { 146 | case Polygon: 147 | p2 := g.(Polygon) 148 | indices := make([]int, len(p2)) 149 | for i := range p2 { 150 | indices[i] = i 151 | } 152 | for _, r1 := range p { 153 | matched := false 154 | for ii, i := range indices { 155 | if ringSimilar(r1, p2[i], tolerance) { // we found a match 156 | matched = true 157 | // remove index i from futher consideration. 158 | if ii == len(indices)-1 { 159 | indices = indices[0:ii] 160 | } else { 161 | indices = append(indices[0:ii], indices[ii+1:len(indices)]...) 162 | } 163 | break 164 | } 165 | } 166 | if !matched { 167 | return false 168 | } 169 | } 170 | return true 171 | default: 172 | return false 173 | } 174 | } 175 | 176 | // Similar determines whether two bounds are similar within tolerance. 177 | func (b *Bounds) Similar(g Geom, tolerance float64) bool { 178 | switch g.(type) { 179 | case *Bounds: 180 | b2 := g.(*Bounds) 181 | return pointSimilar(b.Min, b2.Min, tolerance) && pointSimilar(b.Max, b2.Max, tolerance) 182 | default: 183 | return false 184 | } 185 | } 186 | 187 | // Similar determines whether two geometries collections are similar within tolerance. 188 | // If gc and g have the same geometries 189 | // but in a different order, it will return true. 190 | func (gc GeometryCollection) Similar(g Geom, tolerance float64) bool { 191 | switch g.(type) { 192 | case GeometryCollection: 193 | gc2 := g.(GeometryCollection) 194 | indices := make([]int, len(gc2)) 195 | for i := range gc2 { 196 | indices[i] = i 197 | } 198 | for _, gc1 := range gc { 199 | matched := false 200 | for ii, i := range indices { 201 | if gc1.Similar(gc2[i], tolerance) { // we found a match 202 | matched = true 203 | // remove index i from futher consideration. 204 | if ii == len(indices)-1 { 205 | indices = indices[0:ii] 206 | } else { 207 | indices = append(indices[0:ii], indices[ii+1:len(indices)]...) 208 | } 209 | break 210 | } 211 | } 212 | if !matched { 213 | return false 214 | } 215 | } 216 | return true 217 | default: 218 | return false 219 | } 220 | } 221 | 222 | func ringSimilar(a, b []Point, e float64) bool { 223 | if len(a) != len(b) { 224 | return false 225 | } 226 | ia := minPt(a) 227 | ib := minPt(b) 228 | for i := 0; i < len(a); i++ { 229 | if !pointSimilar(a[ia], b[ib], e) { 230 | return false 231 | } 232 | ia = nextPt(ia, len(a)) 233 | ib = nextPt(ib, len(b)) 234 | } 235 | return true 236 | } 237 | 238 | // ring iterator function 239 | func nextPt(i, l int) int { 240 | if i == l-2 { // Skip the last point that matches the first point. 241 | return 0 242 | } 243 | return i + 1 244 | } 245 | 246 | // find bottom-most of leftmost points, to have fixed anchor 247 | func minPt(c []Point) int { 248 | min := 0 249 | for j, p := range c { 250 | if p.X < c[min].X || p.X == c[min].X && p.Y < c[min].Y { 251 | min = j 252 | } 253 | } 254 | return min 255 | } 256 | --------------------------------------------------------------------------------