mirror of
https://github.com/omniscale/magnacarto.git
synced 2025-02-23 07:54:10 +01:00
270 lines
5.8 KiB
Go
270 lines
5.8 KiB
Go
package builder
|
|
|
|
import (
|
|
"fmt"
|
|
"hash/fnv"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/omniscale/magnacarto/config"
|
|
mmlparse "github.com/omniscale/magnacarto/mml"
|
|
)
|
|
|
|
// MapMaker creates new MapWriters.
|
|
type MapMaker interface {
|
|
New(config.Locator) MapWriter
|
|
// Type returns a unique string for this MapMaker, used for caching generated styles.
|
|
Type() string
|
|
FileSuffix() string
|
|
}
|
|
|
|
type locatorCreator func() config.Locator
|
|
|
|
type style struct {
|
|
mapMaker MapMaker
|
|
mml string
|
|
mss []string
|
|
file string
|
|
lastUpdate time.Time
|
|
}
|
|
|
|
func styleHash(mapType string, mml string, mss []string) uint32 {
|
|
f := fnv.New32()
|
|
f.Write([]byte(mapType))
|
|
f.Write([]byte(mml))
|
|
for i := range mss {
|
|
f.Write([]byte(mss[i]))
|
|
}
|
|
return f.Sum32()
|
|
}
|
|
|
|
func isNewer(file string, timestamp time.Time) bool {
|
|
info, err := os.Stat(file)
|
|
if err != nil {
|
|
return true
|
|
}
|
|
return info.ModTime().After(timestamp)
|
|
}
|
|
|
|
func (s *style) isStale() (bool, error) {
|
|
if s.file == "" {
|
|
return true, nil
|
|
}
|
|
|
|
info, err := os.Stat(s.file)
|
|
if err != nil {
|
|
return true, err
|
|
}
|
|
timestamp := info.ModTime()
|
|
|
|
if isNewer(s.mml, timestamp) {
|
|
return true, nil
|
|
}
|
|
for _, mss := range s.mss {
|
|
if isNewer(mss, timestamp) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
const stylePrefix = "magnacarto-style-"
|
|
|
|
// Cache builds styles and caches the results.
|
|
// It automatically detects changes to the MSS and MML files and rebuilds
|
|
// styles if requested again.
|
|
type Cache struct {
|
|
mu sync.Mutex
|
|
newLocator locatorCreator
|
|
styles map[uint32]*style
|
|
destDir string
|
|
}
|
|
|
|
func NewCache(newLocator locatorCreator) *Cache {
|
|
return &Cache{
|
|
newLocator: newLocator,
|
|
styles: make(map[uint32]*style),
|
|
}
|
|
}
|
|
|
|
func (c *Cache) SetDestination(dest string) {
|
|
c.destDir = dest
|
|
}
|
|
|
|
// ClearAll removes all cached styles.
|
|
// Needs to be called before shutdown to prevent leaking temp files when used _without_ SetDestination.
|
|
// Will remove all cached styles from cache dir when used _with_ SetDestination.
|
|
func (c *Cache) ClearAll() {
|
|
c.ClearTill(time.Now())
|
|
}
|
|
|
|
// ClearTill removes all cached styles that are older then till.
|
|
func (c *Cache) ClearTill(till time.Time) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
if c.destDir != "" {
|
|
// all in same dir. also remove files with different suffixes (e.g. foo.map.font.lst)
|
|
files, err := filepath.Glob(filepath.Join(c.destDir, stylePrefix+"*"))
|
|
if err != nil {
|
|
log.Println("cleanup error: ", err)
|
|
return
|
|
}
|
|
for _, f := range files {
|
|
if fi, err := os.Stat(f); err == nil && fi.ModTime().Before(till) {
|
|
if err := os.Remove(f); err != nil {
|
|
log.Println("cleanup error: ", err)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// with empty destDir, each style is in its own temp dir.
|
|
for _, style := range c.styles {
|
|
if fi, err := os.Stat(style.file); err == nil && fi.ModTime().Before(till) {
|
|
if err := os.RemoveAll(filepath.Dir(style.file)); err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for hash, _ := range c.styles {
|
|
delete(c.styles, hash)
|
|
}
|
|
}
|
|
|
|
type Update struct {
|
|
Err error
|
|
Time time.Time
|
|
UpdatedMML bool
|
|
}
|
|
|
|
// StyleFile returns the filename of the build result. (Re)builds style if required.
|
|
func (c *Cache) StyleFile(mm MapMaker, mml string, mss []string) (string, error) {
|
|
style, err := c.style(mm, mml, mss)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return style.file, nil
|
|
}
|
|
|
|
func (c *Cache) style(mm MapMaker, mml string, mss []string) (*style, error) {
|
|
hash := styleHash(mm.Type(), mml, mss)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if s, ok := c.styles[hash]; ok {
|
|
stale, err := s.isStale()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if stale {
|
|
if len(mss) == 0 {
|
|
var err error
|
|
// refresh mss files
|
|
s.mss, err = mssFilesFromMML(mml)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := c.build(s); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return s, nil
|
|
} else {
|
|
if len(mss) == 0 {
|
|
var err error
|
|
mss, err = mssFilesFromMML(mml)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
s = &style{
|
|
mapMaker: mm,
|
|
mml: mml,
|
|
mss: mss,
|
|
}
|
|
if err := c.build(s); err != nil {
|
|
return nil, err
|
|
}
|
|
c.styles[hash] = s
|
|
return s, nil
|
|
}
|
|
}
|
|
|
|
type FilesMissingError struct {
|
|
Files []string
|
|
}
|
|
|
|
func (e *FilesMissingError) Error() string {
|
|
return fmt.Sprintf("missing files: %v", e.Files)
|
|
}
|
|
|
|
func (c *Cache) build(style *style) error {
|
|
l := c.newLocator()
|
|
l.SetBaseDir(filepath.Dir(style.mml))
|
|
l.SetOutDir(c.destDir)
|
|
l.UseRelPaths(false)
|
|
|
|
m := style.mapMaker.New(l)
|
|
builder := New(m)
|
|
builder.SetIncludeInactive(false)
|
|
|
|
builder.SetMML(style.mml)
|
|
for _, mss := range style.mss {
|
|
builder.AddMSS(mss)
|
|
}
|
|
|
|
if err := builder.Build(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if files := l.MissingFiles(); len(files) > 0 {
|
|
return &FilesMissingError{files}
|
|
}
|
|
|
|
var styleFile string
|
|
if c.destDir != "" {
|
|
hash := styleHash(style.mapMaker.Type(), style.mml, style.mss)
|
|
styleFile = filepath.Join(c.destDir, fmt.Sprintf("magnacarto-style-%d%s", hash, style.mapMaker.FileSuffix()))
|
|
if err := m.WriteFiles(styleFile); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
tmp, err := ioutil.TempDir("", "magnacarto-style")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
styleFile = filepath.Join(tmp, "style"+style.mapMaker.FileSuffix())
|
|
if err := m.WriteFiles(styleFile); err != nil {
|
|
os.RemoveAll(tmp)
|
|
return err
|
|
}
|
|
}
|
|
log.Printf("rebuild style %s as %s with %v\n", style.mml, styleFile, style.mss)
|
|
style.lastUpdate = time.Now()
|
|
style.file = styleFile
|
|
return nil
|
|
}
|
|
|
|
func mssFilesFromMML(mmlFile string) ([]string, error) {
|
|
r, err := os.Open(mmlFile)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer r.Close()
|
|
|
|
mml, err := mmlparse.Parse(r)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mssFiles := []string{}
|
|
for _, s := range mml.Stylesheets {
|
|
mssFiles = append(mssFiles, filepath.Join(filepath.Dir(mmlFile), s))
|
|
}
|
|
return mssFiles, nil
|
|
}
|