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