Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
_obj
_test

# GoLand
.idea/

# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
Expand Down
107 changes: 81 additions & 26 deletions pkg/geojson2svg/geojson2svg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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, `<circle cx="%f" cy="%f" r="1"%s/>`, 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])
Expand All @@ -237,13 +259,13 @@ func drawLineString(sf scaleFunc, w io.Writer, ps [][]float64, attributes string
fmt.Fprintf(w, `<path d="%s"%s/>`, 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")
Expand All @@ -256,7 +278,7 @@ func drawPolygon(sf scaleFunc, w io.Writer, pps [][][]float64, attributes string
fmt.Fprintf(w, `<path d="%s Z"%s/>`, 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)
}
Expand Down Expand Up @@ -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
}
41 changes: 41 additions & 0 deletions pkg/geojson2svg/geojson2svg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
1 change: 1 addition & 0 deletions test/UK_Eire.json
Original file line number Diff line number Diff line change
@@ -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]]]}}]}
1 change: 1 addition & 0 deletions test/UK_Eire.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.