clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

archive/golden-reference
atlas / tools / crawl / axis.go
axis.go 3.6K
1package main23import (4 "bytes"5 "context"6 "fmt"7 "image"8 _ "image/jpeg"9 _ "image/png"10)1112// Most maps address a tile as /{zoom}/{x}/{y}, but some publish the two axes13// the other way round. Nothing in the API says which, and a transposed map14// still returns a real tile for every request, so the mistake is invisible15// until the tiles are laid side by side: neighbours that belong together share16// an edge, and neighbours that do not are unrelated pictures.17//18// probeAxisOrder measures that. It reads one small square block -- the same set19// of URLs either way round -- and reports whether reading the first component20// as the row lines the block up better.21const (22 axisProbeSpan = 323 axisProbeSamples = 3224)2526func probeAxisOrder(27 ctx context.Context,28 fetcher *fetcher,29 base string,30 set apiTileSet,31 zoom int,32 window tileWindow,33 extension string,34) (bool, error) {35 span := axisProbeSpan36 if width := window.maxX - window.minX + 1; span > width {37 span = width38 }39 if height := window.maxY - window.minY + 1; span > height {40 span = height41 }42 if span < 2 {43 return false, nil44 }45 // Sample from the middle, where a map is likeliest to hold content: two46 // blank tiles share an edge perfectly whichever way round they are read.47 originA := window.minX + (window.maxX-window.minX+1-span)/248 originB := window.minY + (window.maxY-window.minY+1-span)/24950 block := make(map[[2]int]image.Image, span*span)51 for a := range span {52 for b := range span {53 url := fmt.Sprintf("%s/games/%s/%d/%d/%d.%s",54 base, set.Path, zoom, originA+a, originB+b, extension)55 body, _, err := fetcher.get(ctx, url)56 if err != nil {57 continue58 }59 decoded, _, err := image.Decode(bytes.NewReader(body))60 if err != nil {61 continue62 }63 block[[2]int{a, b}] = decoded64 }65 }66 if len(block) < span*span {67 // An incomplete probe cannot be trusted either way; assume the usual68 // order rather than transpose on partial evidence.69 return false, nil70 }7172 direct := seamCost(block, false, span)73 transposed := seamCost(block, true, span)74 if direct < 0 || transposed < 0 {75 return false, nil76 }77 return transposed < direct, nil78}7980// seamCost is the average colour distance across the vertical seams of a block,81// laid out either directly or transposed. Lower means the tiles belong together.82func seamCost(block map[[2]int]image.Image, transpose bool, span int) float64 {83 at := func(column, row int) image.Image {84 if transpose {85 return block[[2]int{row, column}]86 }87 return block[[2]int{column, row}]88 }89 var total float6490 var seams int91 for row := range span {92 for column := 0; column < span-1; column++ {93 left, right := at(column, row), at(column+1, row)94 if left == nil || right == nil {95 continue96 }97 total += edgeDistance(left, right)98 seams++99 }100 }101 if seams == 0 {102 return -1103 }104 return total / float64(seams)105}106107// edgeDistance compares the right-hand column of one tile with the left-hand108// column of the tile that should sit beside it.109func edgeDistance(left, right image.Image) float64 {110 lb, rb := left.Bounds(), right.Bounds()111 height := min(lb.Dy(), rb.Dy())112 if height == 0 {113 return 0114 }115 step := max(1, height/axisProbeSamples)116 var total float64117 var count int118 for offset := 0; offset < height; offset += step {119 lr, lg, lbl, _ := left.At(lb.Max.X-1, lb.Min.Y+offset).RGBA()120 rr, rg, rbl, _ := right.At(rb.Min.X, rb.Min.Y+offset).RGBA()121 total += absDiff(lr, rr) + absDiff(lg, rg) + absDiff(lbl, rbl)122 count += 3123 }124 if count == 0 {125 return 0126 }127 return total / float64(count) / 257 // back to 0-255128}129130func absDiff(a, b uint32) float64 {131 if a > b {132 return float64(a - b)133 }134 return float64(b - a)135}