diff --git a/.gitignore b/.gitignore index 2d00d7f..84e5d2b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ _obj _test +# GoLand +.idea/ + # Architecture specific extensions/prefixes *.[568vq] [568vq].out diff --git a/pkg/geojson2svg/geojson2svg.go b/pkg/geojson2svg/geojson2svg.go index 2563b11..ad06a68 100644 --- a/pkg/geojson2svg/geojson2svg.go +++ b/pkg/geojson2svg/geojson2svg.go @@ -13,12 +13,13 @@ import ( "sort" "strings" - geojson "github.com/paulmach/go.geojson" + "github.com/paulmach/go.geojson" ) // TODO release -type scaleFunc func(float64, float64) (float64, float64) +// ScaleFunc accepts x,y coordinates and transforms them, returning a new pair of x,y coordinates. +type ScaleFunc func(float64, float64) (float64, float64) // SVG represents the SVG that should be created. // Use the New function to create a SVG. New will handle the defaualt values. @@ -55,11 +56,17 @@ func New() *SVG { // Draw renders the final SVG with the given options to a string. // All coordinates will be scaled to fit into the svg. func (svg *SVG) Draw(width, height float64, opts ...Option) string { + return svg.DrawWithProjection(width, height, func(x,y float64) (float64, float64){ return x,y}, opts...) +} + +// DrawWithProjection renders the final SVG with the given options to a string. +// All coordinates will be converted by the given projection, then scaled to fit into the svg. +func (svg *SVG) DrawWithProjection(width, height float64, projection ScaleFunc, opts ...Option) string { for _, o := range opts { o(svg) } - sf := makeScaleFunc(width, height, svg.padding, svg.points()) + sf := makeScaleFunc(width, height, svg.padding, svg.points(), projection) content := bytes.NewBufferString("") for _, g := range svg.geometries { @@ -86,30 +93,45 @@ func (svg *SVG) AddGeometry(gs string) error { if err != nil { return fmt.Errorf("invalid geometry: %s", gs) } - svg.geometries = append(svg.geometries, g) + svg.AppendGeometry(g) return nil } +// AppendGeometry adds a geojson Geometry to the svg. +func (svg *SVG) AppendGeometry(f *geojson.Geometry) { + svg.geometries = append(svg.geometries, f) +} + // AddFeature adds a geojson feature to the svg. func (svg *SVG) AddFeature(fs string) error { f, err := geojson.UnmarshalFeature([]byte(fs)) if err != nil { return fmt.Errorf("invalid feature: %s", fs) } - svg.features = append(svg.features, f) + svg.AppendFeature(f) return nil } +// AppendFeature adds a geojson Feature to the svg. +func (svg *SVG) AppendFeature(f *geojson.Feature) { + svg.features = append(svg.features, f) +} + // AddFeatureCollection adds a geojson featurecollection to the svg. func (svg *SVG) AddFeatureCollection(fcs string) error { fc, err := geojson.UnmarshalFeatureCollection([]byte(fcs)) if err != nil { return fmt.Errorf("invalid feature collection: %s", fcs) } - svg.featureCollections = append(svg.featureCollections, fc) + svg.AppendFeatureCollection(fc) return nil } +// AppendFeatureCollection adds a geojson FeatureCollection to the svg. +func (svg *SVG) AppendFeatureCollection(fc *geojson.FeatureCollection) { + svg.featureCollections = append(svg.featureCollections, fc) +} + // WithAttribute adds the key value pair as attribute to the // resulting SVG root element. func WithAttribute(k, v string) Option { @@ -166,7 +188,7 @@ func (svg *SVG) points() [][]float64 { return ps } -func process(sf scaleFunc, w io.Writer, g *geojson.Geometry, attributes string) { +func process(sf ScaleFunc, w io.Writer, g *geojson.Geometry, attributes string) { switch { case g.IsPoint(): drawPoint(sf, w, g.Point, attributes) @@ -217,18 +239,18 @@ func collect(g *geojson.Geometry) (ps [][]float64) { return ps } -func drawPoint(sf scaleFunc, w io.Writer, p []float64, attributes string) { +func drawPoint(sf ScaleFunc, w io.Writer, p []float64, attributes string) { x, y := sf(p[0], p[1]) fmt.Fprintf(w, ``, x, y, attributes) } -func drawMultiPoint(sf scaleFunc, w io.Writer, ps [][]float64, attributes string) { +func drawMultiPoint(sf ScaleFunc, w io.Writer, ps [][]float64, attributes string) { for _, p := range ps { drawPoint(sf, w, p, attributes) } } -func drawLineString(sf scaleFunc, w io.Writer, ps [][]float64, attributes string) { +func drawLineString(sf ScaleFunc, w io.Writer, ps [][]float64, attributes string) { path := bytes.NewBufferString("M") for _, p := range ps { x, y := sf(p[0], p[1]) @@ -237,13 +259,13 @@ func drawLineString(sf scaleFunc, w io.Writer, ps [][]float64, attributes string fmt.Fprintf(w, ``, trim(path), attributes) } -func drawMultiLineString(sf scaleFunc, w io.Writer, pps [][][]float64, attributes string) { +func drawMultiLineString(sf ScaleFunc, w io.Writer, pps [][][]float64, attributes string) { for _, ps := range pps { drawLineString(sf, w, ps, attributes) } } -func drawPolygon(sf scaleFunc, w io.Writer, pps [][][]float64, attributes string) { +func drawPolygon(sf ScaleFunc, w io.Writer, pps [][][]float64, attributes string) { path := bytes.NewBufferString("") for _, ps := range pps { subPath := bytes.NewBufferString("M") @@ -256,7 +278,7 @@ func drawPolygon(sf scaleFunc, w io.Writer, pps [][][]float64, attributes string fmt.Fprintf(w, ``, trim(path), attributes) } -func drawMultiPolygon(sf scaleFunc, w io.Writer, ppps [][][][]float64, attributes string) { +func drawMultiPolygon(sf ScaleFunc, w io.Writer, ppps [][][][]float64, attributes string) { for _, pps := range ppps { drawPolygon(sf, w, pps, attributes) } @@ -290,33 +312,66 @@ func makeAttributesFromProperties(useProp func(string) bool, props map[string]in return makeAttributes(attrs) } -func makeScaleFunc(width, height float64, padding Padding, ps [][]float64) scaleFunc { +func makeScaleFunc(width, height float64, padding Padding, ps [][]float64, projection ScaleFunc) ScaleFunc { w := width - padding.Left - padding.Right - h := width - padding.Top - padding.Bottom + h := height - padding.Top - padding.Bottom if len(ps) == 0 { - return func(x, y float64) (float64, float64) { return x, y } + return func(x, y float64) (float64, float64) { return projection(x, y) } } if len(ps) == 1 { return func(x, y float64) (float64, float64) { return w / 2, h / 2 } } - minX := ps[0][0] - maxX := ps[0][0] - minY := ps[0][1] - maxY := ps[0][1] - for _, p := range ps[1:] { - minX = math.Min(minX, p[0]) - maxX = math.Max(maxX, p[0]) - minY = math.Min(minY, p[1]) - maxY = math.Max(maxY, p[1]) - } + minX, minY, maxX, maxY := getBoundingRectangle(projection, ps) xRes := (maxX - minX) / w yRes := (maxY - minY) / h res := math.Max(xRes, yRes) return func(x, y float64) (float64, float64) { + x, y = projection(x, y) return (x-minX)/res + padding.Left, (maxY-y)/res + padding.Top } + +} + +func getBoundingRectangle(projection ScaleFunc, ps [][]float64) (float64, float64, float64, float64) { + minX, minY := projection(ps[0][0], ps[0][1]) + maxX, maxY := projection(ps[0][0], ps[0][1]) + for _, p := range ps[1:] { + x, y := projection(p[0], p[1]) + minX = math.Min(minX, x) + maxX = math.Max(maxX, x) + minY = math.Min(minY, y) + maxY = math.Max(maxY, y) + } + return minX, minY, maxX, maxY +} + +// GetHeightForWidth returns an appropriate height given a desired width. +func (svg *SVG) GetHeightForWidth(width float64, projection ScaleFunc) float64 { + minX, minY, maxX, maxY := getBoundingRectangle(projection, svg.points()) + svgWidth := maxX - minX; + svgHeight := maxY - minY; + ratio := svgHeight / svgWidth; + return math.Floor((width * ratio) + .5) + +} + +// MercatorProjection is a projection function that will convert latitude & logitude into x,y coordinates for a Mercator map. +var MercatorProjection = func(longitude, latitude float64) (float64, float64) { + // https://stackoverflow.com/questions/38270132/topojson-d3-map-with-longitude-latitude + mapWidth, mapHeight := 100.0, 100.0 + // get x value + x := (longitude + 180) * (mapWidth / 360) + + // convert from degrees to radians + latRad := latitude * math.Pi / 180 + + // get y value + mercN := math.Log(math.Tan((math.Pi / 4) + (latRad / 2))) + y := (mapHeight / 2) - (mapHeight * mercN / (2 * math.Pi)) + // invert the y-axis to put the map the right way up + return x, mapHeight - y } diff --git a/pkg/geojson2svg/geojson2svg_test.go b/pkg/geojson2svg/geojson2svg_test.go index 11854a3..ee6f2d5 100644 --- a/pkg/geojson2svg/geojson2svg_test.go +++ b/pkg/geojson2svg/geojson2svg_test.go @@ -584,3 +584,44 @@ func TestExample(t *testing.T) { t.Errorf("expected %s, got %s", string(want), got) } } + +func TestUkEire(t *testing.T) { + exampleFile := path.Join("..", "..", "test", "UK_Eire.json") + geojson, err := ioutil.ReadFile(exampleFile) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + svgFile := path.Join("..", "..", "test", "UK_Eire.svg") + want, err := ioutil.ReadFile(svgFile) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + svg := geojson2svg.New() + err = svg.AddFeatureCollection(string(geojson)) + if err != nil { + t.Fatalf("unexpected error %v", err) + } + + width := 1000.0 + height := svg.GetHeightForWidth(width, geojson2svg.MercatorProjection) + + if (height != 1281.0) { + t.Errorf("expected %f, got %f", 1281.0, height) + } + + got := svg.DrawWithProjection(width, height, geojson2svg.MercatorProjection, + geojson2svg.WithAttribute("xmlns", "http://www.w3.org/2000/svg"), + geojson2svg.UseProperties([]string{"style"}), + geojson2svg.WithPadding(geojson2svg.Padding{ + Top: 10, + Right: 10, + Bottom: 10, + Left: 10, + })) + + if got != string(want) { + t.Errorf("expected %s, got %s", string(want), got) + } +} diff --git a/test/UK_Eire.json b/test/UK_Eire.json new file mode 100644 index 0000000..7a01343 --- /dev/null +++ b/test/UK_Eire.json @@ -0,0 +1 @@ +{"type":"FeatureCollection","features":[{"type":"Feature","properties":{"scalerank":1,"featurecla":"Admin-0 country","labelrank":2,"sovereignt":"United Kingdom","sov_a3":"GB1","adm0_dif":1,"level":2,"type":"Country","admin":"United Kingdom","adm0_a3":"GBR","geou_dif":0,"geounit":"United Kingdom","gu_a3":"GBR","su_dif":0,"subunit":"United Kingdom","su_a3":"GBR","brk_diff":0,"name":"United Kingdom","name_long":"United Kingdom","brk_a3":"GBR","brk_name":"United Kingdom","brk_group":null,"abbrev":"U.K.","postal":"GB","formal_en":"United Kingdom of Great Britain and Northern Ireland","formal_fr":null,"note_adm0":null,"note_brk":null,"name_sort":"United Kingdom","name_alt":null,"mapcolor7":6,"mapcolor8":6,"mapcolor9":6,"mapcolor13":3,"pop_est":62262000,"gdp_md_est":1977704,"pop_year":0,"lastcensus":2011,"gdp_year":2009,"economy":"1. Developed region: G7","income_grp":"1. High income: OECD","wikipedia":-99,"fips_10":null,"iso_a2":"GB","iso_a3":"GBR","iso_n3":"826","un_a3":"826","wb_a2":"GB","wb_a3":"GBR","woe_id":-99,"adm0_a3_is":"GBR","adm0_a3_us":"GBR","adm0_a3_un":-99,"adm0_a3_wb":-99,"continent":"Europe","region_un":"Europe","subregion":"Northern Europe","region_wb":"Europe & Central Asia","name_len":14,"long_len":14,"abbrev_len":4,"tiny":-99,"homepart":1,"filename":"GBR.geojson"},"geometry":{"type":"MultiPolygon","coordinates":[[[[-5.661948614921897,54.55460317648385],[-6.197884894220977,53.86756500916334],[-6.953730231137996,54.073702297575636],[-7.572167934591079,54.05995636658599],[-7.366030646178785,54.595840969452695],[-7.572167934591079,55.1316222194549],[-6.733847011736145,55.1728600124238],[-5.661948614921897,54.55460317648385]]],[[[-3.005004848635281,58.63500010846633],[-4.073828497728016,57.55302480735525],[-3.055001796877661,57.69001902936095],[-1.959280564776918,57.68479970969951],[-2.219988165689301,56.87001740175353],[-3.119003058271118,55.973793036515474],[-2.085009324543023,55.90999848085127],[-2.005675679673857,55.80490285035023],[-1.11499101399221,54.62498647726539],[-0.4304849918542,54.46437612570216],[0.184981316742039,53.32501414653103],[0.469976840831777,52.92999949809197],[1.681530795914739,52.739520168664],[1.559987827164377,52.09999848083601],[1.050561557630914,51.806760565795685],[1.449865349950301,51.28942780212196],[0.550333693045502,50.765738837275876],[-0.78751746255864,50.77498891865622],[-2.489997524414377,50.50001862243124],[-2.956273972984036,50.696879991247016],[-3.617448085942328,50.22835561787272],[-4.542507900399244,50.34183706318566],[-5.245023159191135,49.95999990498108],[-5.776566941745301,50.15967763935682],[-4.309989793301838,51.21000112568916],[-3.414850633142123,51.42600861266925],[-3.422719467108323,51.42684816740609],[-4.984367234710874,51.593466091510976],[-5.267295701508885,51.99140045837458],[-4.222346564134853,52.301355699261364],[-4.770013393564113,52.840004991255626],[-4.579999152026915,53.49500377055517],[-3.093830673788659,53.404547400669685],[-3.092079637047106,53.404440822963544],[-2.945008510744344,53.984999701546684],[-3.614700825433034,54.600936773292574],[-3.63000545898933,54.615012925833014],[-4.844169073903004,54.790971177786844],[-5.082526617849226,55.06160065369937],[-4.719112107756644,55.50847260194348],[-5.047980922862109,55.78398550070752],[-5.586397670911139,55.31114614523682],[-5.644998745130181,56.275014960344805],[-6.149980841486354,56.78500967063354],[-5.786824713555291,57.81884837506465],[-5.009998745127575,58.63001333275005],[-4.211494513353557,58.55084503847917],[-3.005004848635281,58.63500010846633]]]]}},{"type":"Feature","properties":{"scalerank":1,"featurecla":"Admin-0 country","labelrank":3,"sovereignt":"Ireland","sov_a3":"IRL","adm0_dif":0,"level":2,"type":"Sovereign country","admin":"Ireland","adm0_a3":"IRL","geou_dif":0,"geounit":"Ireland","gu_a3":"IRL","su_dif":0,"subunit":"Ireland","su_a3":"IRL","brk_diff":0,"name":"Ireland","name_long":"Ireland","brk_a3":"IRL","brk_name":"Ireland","brk_group":null,"abbrev":"Ire.","postal":"IRL","formal_en":"Ireland","formal_fr":null,"note_adm0":null,"note_brk":null,"name_sort":"Ireland","name_alt":null,"mapcolor7":2,"mapcolor8":3,"mapcolor9":2,"mapcolor13":2,"pop_est":4203200,"gdp_md_est":188400,"pop_year":-99,"lastcensus":2011,"gdp_year":-99,"economy":"2. Developed region: nonG7","income_grp":"1. High income: OECD","wikipedia":-99,"fips_10":null,"iso_a2":"IE","iso_a3":"IRL","iso_n3":"372","un_a3":"372","wb_a2":"IE","wb_a3":"IRL","woe_id":-99,"adm0_a3_is":"IRL","adm0_a3_us":"IRL","adm0_a3_un":-99,"adm0_a3_wb":-99,"continent":"Europe","region_un":"Europe","subregion":"Northern Europe","region_wb":"Europe & Central Asia","name_len":7,"long_len":7,"abbrev_len":4,"tiny":-99,"homepart":1,"filename":"IRL.geojson"},"geometry":{"type":"Polygon","coordinates":[[[-6.197884894220991,53.86756500916336],[-6.03298539877761,53.15316417094435],[-6.788856573910849,52.260117906292336],[-8.56161658368356,51.669301255899356],[-9.977085740590269,51.82045482035307],[-9.16628251793078,52.86462881124268],[-9.688524542672454,53.8813626165853],[-8.327987433292009,54.66451894796863],[-7.572167934591064,55.13162221945487],[-7.366030646178785,54.59584096945272],[-7.572167934591064,54.059956366586],[-6.953730231138067,54.073702297575636],[-6.197884894220991,53.86756500916336]]]}}]} \ No newline at end of file diff --git a/test/UK_Eire.svg b/test/UK_Eire.svg new file mode 100755 index 0000000..9da79b5 --- /dev/null +++ b/test/UK_Eire.svg @@ -0,0 +1 @@ + \ No newline at end of file