diff --git a/api/api.go b/api/api.go index 7f2668f..ed88b97 100755 --- a/api/api.go +++ b/api/api.go @@ -64,6 +64,10 @@ func NewAPI(e *echo.Echo, s *store.Store) { api.GET("/swagger", func(c echo.Context) error { return c.Redirect(http.StatusMovedPermanently, "/swagger/index.html") }) + + // style + api.Static("/style", "./style") + api.Register() for _, route := range api.Routes() { diff --git a/main.go b/main.go index d28ea0a..d65e7ba 100755 --- a/main.go +++ b/main.go @@ -32,7 +32,7 @@ func testSprite() { panic(err) } - spriteBuilder.Build() + spriteBuilder.Build("icons") } func testFont() { @@ -46,7 +46,9 @@ func testFont() { panic(err) } - fonts.ToPBF() + fonts.Build("Noto Sans Bold") + fonts.Build("Noto Sans Italic") + fonts.Build("Noto Sans Regular") } func testTiles() { diff --git a/util/fontnik/fontnik.go b/util/fontnik/fontnik.go index 0921de7..269ac7e 100644 --- a/util/fontnik/fontnik.go +++ b/util/fontnik/fontnik.go @@ -4,9 +4,11 @@ import ( "fmt" "image" "image/draw" + "log/slog" "math" "os" "path/filepath" + "sort" "strings" "git.zhouxhere.com/zhouxhere/maptile/protobuf" @@ -38,8 +40,10 @@ type FontFace struct { } type Fontnik struct { - outPath string - FontFaces []*FontFace + outPath string + // Name string + // FontFaces []*FontFace + FontMaps map[string][]*FontFace } func NewFontnik(fontPath, outPath string) (*Fontnik, error) { @@ -58,110 +62,122 @@ func NewFontnik(fontPath, outPath string) (*Fontnik, error) { files, err := os.ReadDir(fontPath) if err != nil { - panic(err) + return nil, fmt.Errorf("读取字体目录失败: %v", err) } - fontFaces := []*FontFace{} + fontMap := make(map[string][]*FontFace) for _, file := range files { - if file.IsDir() { - continue - } filePath := filepath.Join(fontPath, file.Name()) - - if !strings.HasSuffix(file.Name(), ".ttf") && !strings.HasSuffix(file.Name(), ".otf") { - // return nil, errors.Errorf("font file not support %s", file.Name()) + if file.IsDir() { + faces := readFonts(filePath) + fontMap[file.Name()] = faces continue } - fontBytes, err := os.ReadFile(filePath) + fontFace, err := readFont(filePath) if err != nil { - // return nil, fmt.Errorf("读取字体文件失败: %v", err) + slog.Error("Failed to read font file %s: %v", filePath, err) continue } - openFont, err := opentype.Parse(fontBytes) - if err != nil { - // return nil, fmt.Errorf("解析字体失败: %v", err) - continue - } + fileName := strings.TrimSuffix(file.Name(), filepath.Ext(file.Name())) - fontFamily, err := openFont.Name(&sfnt.Buffer{}, sfnt.NameIDFamily) - if err != nil { - return nil, err - } - - fontSubFamily, err := openFont.Name(&sfnt.Buffer{}, sfnt.NameIDSubfamily) - if err != nil { - return nil, err - } - - fontFamily = fmt.Sprintf("%s %s", fontFamily, fontSubFamily) - - face, err := opentype.NewFace(openFont, &opentype.FaceOptions{ - Size: DefaultFontSize, - DPI: 72, - Hinting: font.HintingFull, - }) - if err != nil { - return nil, err - } - - metrics := face.Metrics() - // fontDesignedHeight := metrics.Ascent.Floor() + metrics.Descent.Floor() - // fixed := int(math.Round(float64(metrics.Height.Floor()-fontDesignedHeight)/2)) + 1 - - fontFaces = append(fontFaces, &FontFace{ - Name: fontFamily, - Font: face, - // yStart: metrics.Height.Floor() + metrics.Descent.Floor() + fixed, - yStart: metrics.Ascent.Floor(), - // maxCode: openFont.NumGlyphs(), - maxCode: DefaultMaxCode, - }) + fontMap[fileName] = append(fontMap[fileName], fontFace) } return &Fontnik{ - outPath: filepath.Join(filepath.Dir(basePath), outPath), - FontFaces: fontFaces, + outPath: filepath.Join(filepath.Dir(basePath), outPath), + // FontFaces: fontFaces, + FontMaps: fontMap, }, nil } -func (f *Fontnik) ToPBF() { +func (f *Fontnik) Build(name string) error { + if _, ok := f.FontMaps[name]; !ok { + return fmt.Errorf("font %s not found", name) + } - if _, err := os.Stat(f.outPath); err != nil { - err = os.MkdirAll(f.outPath, 0644) + path := filepath.Join(f.outPath, name) + if _, err := os.Stat(path); err != nil { + err = os.MkdirAll(path, 0644) if err != nil { - panic(err) + return err } } - for _, face := range f.FontFaces { + var errs []error - if _, err := os.Stat(filepath.Join(f.outPath, face.Name)); err != nil { - err = os.MkdirAll(filepath.Join(f.outPath, face.Name), 0644) - if err != nil { - panic(err) + for start := 0; start <= DefaultMaxCode; start += DefaultBlockSize { + + endCode := min(start+DefaultBlockSize-1, DefaultMaxCode) + + stackRange := fmt.Sprintf("%d-%d", start, endCode) + fontstack := &protobuf.Fontstack{ + Name: &name, + Range: &stackRange, + Glyphs: []*protobuf.Glyph{}, + } + for _, face := range f.FontMaps[name] { + if face.maxCode < start { + continue } + + tmpGlyphs, err := face.ProcessRange(start, endCode) + if err != nil { + errs = append(errs, err) + slog.Error("Failed to process font %s range: %v", name, err) + continue + } + + fontstack.Glyphs = append(fontstack.Glyphs, tmpGlyphs...) } - fmt.Println("Font:", face.Name, "MaxCode:", face.maxCode) + if len(f.FontMaps[name]) > 1 { + sort.Slice(fontstack.Glyphs, func(i, j int) bool { + return fontstack.Glyphs[i].GetId() < fontstack.Glyphs[j].GetId() + }) + } - for start := 0; start <= int(face.maxCode); start += DefaultBlockSize { - - endCode := min(start+DefaultBlockSize-1, face.maxCode) - - stack, err := face.ProcessRange(start, endCode) - if err != nil { - return - } - - if err := f.SaveStack(stack); err != nil { - fmt.Println(err) - return - } + if err := f.SaveStack(fontstack); err != nil { + errs = append(errs, err) + slog.Error("Failed to save font %s stack: %v", name, err) + continue } } + + if len(errs) > 0 { + return fmt.Errorf("build font %s failed: %v", name, errs) + } + + return nil + + // for _, face := range f.FontFaces { + + // if _, err := os.Stat(filepath.Join(f.outPath, face.Name)); err != nil { + // err = os.MkdirAll(filepath.Join(f.outPath, face.Name), 0644) + // if err != nil { + // panic(err) + // } + // } + + // fmt.Println("Font:", face.Name, "MaxCode:", face.maxCode) + + // for start := 0; start <= int(face.maxCode); start += DefaultBlockSize { + + // endCode := min(start+DefaultBlockSize-1, face.maxCode) + + // stack, err := face.ProcessRange(start, endCode) + // if err != nil { + // return + // } + + // if err := f.SaveStack(stack); err != nil { + // fmt.Println(err) + // return + // } + // } + // } } func (f *Fontnik) SaveStack(stack *protobuf.Fontstack) error { @@ -178,24 +194,26 @@ func (f *Fontnik) SaveStack(stack *protobuf.Fontstack) error { return os.WriteFile(filename, data, 0644) } -func (ff *FontFace) ProcessRange(start, end int) (*protobuf.Fontstack, error) { - fontRange := fmt.Sprintf("%d-%d", start, end) - stack := &protobuf.Fontstack{ - Name: &ff.Name, - Range: &fontRange, - Glyphs: []*protobuf.Glyph{}, - } +func (ff *FontFace) ProcessRange(start, end int) ([]*protobuf.Glyph, error) { + // fontRange := fmt.Sprintf("%d-%d", start, end) + // stack := &protobuf.Fontstack{ + // Name: &ff.Name, + // Range: &fontRange, + // Glyphs: []*protobuf.Glyph{}, + // } + glyphs := []*protobuf.Glyph{} for code := start; code <= end; code++ { glyph := ff.RenderGlyph(rune(code)) if glyph != nil { - stack.Glyphs = append(stack.Glyphs, glyph) + // stack.Glyphs = append(stack.Glyphs, glyph) + glyphs = append(glyphs, glyph) } } - return stack, nil + return glyphs, nil } func (ff *FontFace) RenderGlyph(code rune) *protobuf.Glyph { @@ -367,3 +385,82 @@ func edt1d(f []float64, d []float64, v []float64, z []float64, n int) { d[q] = (float64(q)-v[k])*(float64(q)-v[k]) + f[int(v[k])] } } + +func readFonts(path string) []*FontFace { + var files []string + ttfFiles, err := filepath.Glob(path + "/*.ttf") + if err != nil { + slog.Error("Failed to read ttf files from path %s: %v", path, err) + } + files = append(files, ttfFiles...) + otfFiles, err := filepath.Glob(path + "/*.otf") + if err != nil { + slog.Error("Failed to read otf files from path %s: %v", path, err) + } + files = append(files, otfFiles...) + + var fontFaces []*FontFace + + for _, file := range files { + + fontFace, err := readFont(file) + if err != nil { + slog.Error("Failed to read font file %s: %v", file, err) + continue + } + + fontFaces = append(fontFaces, fontFace) + } + + return fontFaces +} + +func readFont(path string) (*FontFace, error) { + if !strings.HasSuffix(path, ".ttf") && !strings.HasSuffix(path, ".otf") { + return nil, fmt.Errorf("unsupported font format: %s", path) + } + + fontBytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + openFont, err := opentype.Parse(fontBytes) + if err != nil { + return nil, err + } + + fontFamily, err := openFont.Name(&sfnt.Buffer{}, sfnt.NameIDFamily) + if err != nil { + return nil, err + } + + fontSubFamily, err := openFont.Name(&sfnt.Buffer{}, sfnt.NameIDSubfamily) + if err != nil { + return nil, err + } + + fontFamily = fmt.Sprintf("%s %s", fontFamily, fontSubFamily) + face, err := opentype.NewFace(openFont, &opentype.FaceOptions{ + Size: DefaultFontSize, + DPI: 72, + Hinting: font.HintingFull, + }) + + if err != nil { + return nil, err + } + + metrics := face.Metrics() + // fontDesignedHeight := metrics.Ascent.Floor() + metrics.Descent.Floor() + // fixed := int(math.Round(float64(metrics.Height.Floor()-fontDesignedHeight)/2)) + 1 + + return &FontFace{ + Name: fontFamily, + Font: face, + // yStart: metrics.Height.Floor() + metrics.Descent.Floor() + fixed, + yStart: metrics.Ascent.Floor(), + // maxCode: openFont.NumGlyphs(), + maxCode: DefaultMaxCode, + }, nil +} diff --git a/util/sprite/sprite.go b/util/sprite/sprite.go index 7d1ae78..90f34fd 100644 --- a/util/sprite/sprite.go +++ b/util/sprite/sprite.go @@ -63,7 +63,7 @@ func NewSpriteBuilder(iconPath string, outPath string) (*SpriteBuilder, error) { files, err := os.ReadDir(iconPath) if err != nil { - panic(err) + return nil, err } sb := &SpriteBuilder{ @@ -113,27 +113,58 @@ func NewSpriteBuilder(iconPath string, outPath string) (*SpriteBuilder, error) { return sb, nil } -func (sb *SpriteBuilder) Build() error { - for _, sprite := range sb.sprites { - - if _, err := os.Stat(filepath.Join(sb.outPath, sprite.name)); err != nil { - err = os.MkdirAll(filepath.Join(sb.outPath, sprite.name), 0644) - if err != nil { - return err - } +func (sb *SpriteBuilder) Build(name string) error { + // 查找name为name的sprite + var sprite *Sprite + for _, s := range sb.sprites { + if s.name == name { + sprite = &s + break } + } - sprite.Fit() + if sprite == nil { + return fmt.Errorf("sprite %s not found", name) + } - if err := sprite.Save(sb.outPath, 1); err != nil { - return err - } - - if err := sprite.Save(sb.outPath, 2); err != nil { + if _, err := os.Stat(filepath.Join(sb.outPath, sprite.name)); err != nil { + err = os.MkdirAll(filepath.Join(sb.outPath, sprite.name), 0644) + if err != nil { return err } } + + sprite.Fit() + + if err := sprite.Save(sb.outPath, 1); err != nil { + return err + } + + if err := sprite.Save(sb.outPath, 2); err != nil { + return err + } + return nil + // for _, sprite := range sb.sprites { + + // if _, err := os.Stat(filepath.Join(sb.outPath, sprite.name)); err != nil { + // err = os.MkdirAll(filepath.Join(sb.outPath, sprite.name), 0644) + // if err != nil { + // return err + // } + // } + + // sprite.Fit() + + // if err := sprite.Save(sb.outPath, 1); err != nil { + // return err + // } + + // if err := sprite.Save(sb.outPath, 2); err != nil { + // return err + // } + // } + // return nil } func (s *Sprite) Fit() { diff --git a/web/components/maplibre/code.vue b/web/components/maplibre/code.vue new file mode 100644 index 0000000..8d780f0 --- /dev/null +++ b/web/components/maplibre/code.vue @@ -0,0 +1,63 @@ + + + + + diff --git a/web/components/maplibre/editor.vue b/web/components/maplibre/editor.vue new file mode 100644 index 0000000..edcdb0f --- /dev/null +++ b/web/components/maplibre/editor.vue @@ -0,0 +1,54 @@ + + + diff --git a/web/components/maplibre/map.vue b/web/components/maplibre/map.vue index 8f9d34a..ba680f0 100644 --- a/web/components/maplibre/map.vue +++ b/web/components/maplibre/map.vue @@ -4,6 +4,9 @@ import "maplibre-gl/dist/maplibre-gl.css"; import * as turf from "@turf/turf"; import MapboxDraw from "@mapbox/mapbox-gl-draw"; import "@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css"; +import Code from "./code.vue"; +import Editor from "./editor.vue"; +import { StyleSpecification } from "maplibre-gl"; const mapContainer = useTemplateRef("mapContainer"); @@ -24,12 +27,23 @@ MapboxDraw.lib.theme.find((x) => x.id === "gl-draw-lines").paint[ "line-dasharray" ] = [0.2, 2]; +/** + * @type {Ref} + */ +const styleJSON = ref({ + version: 8, + sources: {}, + layers: [], +}); + onMounted(async () => { + styleJSON.value = await fetch("/style.json").then((res) => res.json()); if (!mapContainer.value) return; map.value = new maplibregl.Map({ container: mapContainer.value, // container id // style: "https://demotiles.maplibre.org/style.json", // style URL - style: "/style.json", + // style: "/style.json", + style: styleJSON.value, center: [120.147376, 30.272934], // starting position [lng, lat] zoom: 7, // starting zoom }); @@ -73,17 +87,17 @@ onUnmounted(() => { const featureResult = await feature.list({ page: 1, size: 10 }); const handleData = () => { - // if (featureResult.status.value !== "success") return; - // let jsonData = turf.featureCollection( - // featureResult.data.value.data.map((x) => - // turf.feature(x.geometry, { - // id: x.id, - // name: x.name, - // }) - // ) - // ); + if (featureResult.status.value !== "success") return; + let jsonData = turf.featureCollection( + featureResult.data.value.data.map((x) => + turf.feature(x.geometry, { + id: x.id, + name: x.name, + }) + ) + ); - // map.value.addSource("data", { type: "geojson", data: jsonData }); + map.value.addSource("data", { type: "geojson", data: jsonData }); // map.value.addLayer({ // id: "data-symbol", // type: "symbol", @@ -93,7 +107,6 @@ const handleData = () => { // }, // layout: { // "text-field": ["get", "name"], - // "text-font": ["Open Sans Regular", "Arial Unicode MS Regular"], // "text-size": 12, // }, // }); @@ -107,97 +120,26 @@ const handleData = () => { // "fill-outline-color": "#FF0000", // 添加边界线颜色 // }, // }); +}; - // map.value.addSource("zhejiang-pm", { - // type: "vector", - // url: "/api/v1/pmtiles/zhejiang-pm", - // // url: "http://localhost:8080/zhejiang-pm.json" - // }); - // map.value.addLayer({ - // id: "water", - // type: "fill", - // filter: ["==", "$type", "Polygon"], - // source: "zhejiang-pm", - // "source-layer": "water", - // paint: { - // "fill-color": "#80deea", - // }, - // }); - // map.value.addLayer({ - // id: "water_river", - // type: "line", - // source: "zhejiang-pm", - // "source-layer": "water", - // minzoom: 9, - // filter: ["in", "kind", "river"], - // paint: { - // "line-color": "#80deea", - // "line-width": [ - // "interpolate", - // ["exponential", 1.6], - // ["zoom"], - // 9, - // 0, - // 9.5, - // 1, - // 18, - // 12, - // ], - // }, - // }); - // map.value.addLayer({ - // id: "world_cities", - // type: "symbol", - // minzoom: 0, - // maxzoom: 6, - // source: "world_cities", - // "source-layer": "cities", - // layout: { - // "text-field": "{name}", - // // "text-font": ["Open Sans Regular", "Arial Unicode MS Regular"], - // "text-size": 12, - // }, - // paint: { - // "text-color": "rgb(255,0,0)", - // }, - // }); - - // map.value.addSource("china-shortbread", { - // type: "vector", - // // url: "/api/v1/mbtiles/china-shortbread", - // bounds: [73.41788, 14.27437, 134.8036, 53.65559], - // maxzoom: 14, - // minzoom: 0, - // tiles: [ - // "http://localhost:8888/api/v1/mbtiles/china-shortbread/{z}/{x}/{y}", - // ], - // }); - - // map.value.addLayer({ - // id: "china-shortbread", - // type: "symbol", - // bounds: [73.41788, 14.27437, 134.8036, 53.65559], - // maxzoom: 14, - // minzoom: 0, - // source: "china-shortbread", - // "source-layer": "place_labels", - // layout: { - // "text-field": "hello", - // // "text-font": ["Open Sans Regular", "Arial Unicode MS Regular"], - // "text-size": 12, - // }, - // paint: { - // "text-color": "#000", - // }, - // }); - - setTimeout(() => { - let styles = map.value.getStyle(); - console.log(styles); - }, 1000); +const handleCodesChange = (codes) => { + if (!map.value) return; + console.log(codes); + map.value.setStyle(codes); }; diff --git a/web/layouts/default.vue b/web/layouts/default.vue index 01b3df2..5866cb8 100644 --- a/web/layouts/default.vue +++ b/web/layouts/default.vue @@ -28,7 +28,7 @@ const route = useRoute();