Examples
Practical code examples for the ENC Chart Manager.
All examples in this document are available as complete, compilable programs in the docs/examples/ directory. Each example includes its own go.mod
and can be run with go run main.go
.
Quick Start
Complete, compilable version: docs/examples/quick-start/main.go
package main
import (
"fmt"
"log"
"github.com/beetlebugorg/chartmanager/pkg/chartmanager"
"github.com/beetlebugorg/s57/pkg/s57"
)
func main() {
// Create manager with defaults
mgr, err := chartmanager.NewChartManager(
chartmanager.DefaultChartManagerOptions(),
)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Chart manager ready: %d charts indexed\n", mgr.ChartCount())
// Query catalog for San Francisco Bay
viewport := s57.Bounds{
MinLon: -122.5, MaxLon: -122.0,
MinLat: 37.5, MaxLat: 38.0,
}
entries := mgr.QueryCatalog(viewport)
fmt.Printf("Found %d charts for SF Bay\n", len(entries))
// Load charts for viewport (downloads on-demand)
charts, err := mgr.GetChartsForViewport(viewport, 12)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Loaded %d charts\n", len(charts))
}
To run:
cd docs/examples/quick-start && go run main.go
Query Before Download
Complete, compilable version: docs/examples/query-before-download/main.go
Preview what charts are available before downloading them:
func queryBeforeDownload(mgr *chartmanager.ChartManager) {
// Define area of interest
viewport := s57.Bounds{
MinLon: -122.5, MaxLon: -122.0,
MinLat: 37.5, MaxLat: 38.0,
}
// Step 1: Query catalog (NO downloads)
entries := mgr.QueryCatalog(viewport)
// Calculate total size
var totalSizeMB float64
for _, entry := range entries {
totalSizeMB += entry.SizeMB
}
fmt.Printf("Found %d charts, %.1f MB total\n", len(entries), totalSizeMB)
// Show details
for _, entry := range entries {
fmt.Printf(" %s - %.1f MB - Scale 1:%d - %s\n",
entry.Name, entry.SizeMB, entry.Scale, entry.UsageBand())
}
// Step 2: User decides whether to proceed
fmt.Print("\nDownload these charts? (y/n): ")
var response string
fmt.Scanln(&response)
if response == "y" {
// Step 3: Now download and load
charts, err := mgr.GetChartsForViewport(viewport, 12)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Downloaded and loaded %d charts\n", len(charts))
} else {
fmt.Println("Skipping download")
}
}
Bandwidth-Conscious Application
Build an application that minimizes unnecessary downloads:
func bandwidthConsciousApp(mgr *chartmanager.ChartManager) {
viewport := s57.Bounds{
MinLon: -74.1, MaxLon: -73.9,
MinLat: 40.6, MaxLat: 40.8,
}
// Query catalog first (no download)
entries := mgr.QueryCatalog(viewport)
// Let user select specific charts
fmt.Println("Available charts:")
for i, entry := range entries {
fmt.Printf("[%d] %s - %.1f MB - %s\n",
i+1, entry.Name, entry.SizeMB, entry.Title)
}
// User selects which charts to download
fmt.Print("\nEnter chart numbers to download (e.g., 1,3,5): ")
var selection string
fmt.Scanln(&selection)
// Parse selection and download only selected charts
selectedIndices := parseSelection(selection)
for _, idx := range selectedIndices {
if idx >= 0 && idx < len(entries) {
entry := entries[idx]
fmt.Printf("Downloading %s...\n", entry.Name)
chart, err := mgr.GetChart(entry.Name)
if err != nil {
log.Printf("Failed to download %s: %v", entry.Name, err)
continue
}
fmt.Printf(" Loaded %s: %d features\n",
chart.DatasetName(), chart.FeatureCount())
}
}
}
func parseSelection(s string) []int {
// Simple parser for comma-separated numbers
// Implementation left as exercise
return []int{}
}
Catalog Queries
Query the catalog to see what charts are available before downloading:
func exploreArea(mgr *chartmanager.ChartManager) {
// Define area of interest
viewport := s57.Bounds{
MinLon: -76.6, MaxLon: -76.4,
MinLat: 38.9, MaxLat: 39.0,
}
// Query catalog (no download)
entries := mgr.QueryCatalog(viewport)
fmt.Printf("Charts covering Annapolis:\n")
for _, entry := range entries {
fmt.Printf(" %s - %s\n", entry.Name, entry.Title)
fmt.Printf(" Scale: 1:%d\n", entry.Scale)
fmt.Printf(" Size: %.1f MB\n", entry.SizeMB)
fmt.Printf(" Band: %s\n", entry.UsageBand())
fmt.Printf(" Bounds: [%.4f,%.4f] to [%.4f,%.4f]\n\n",
entry.Bounds.MinLon, entry.Bounds.MinLat,
entry.Bounds.MaxLon, entry.Bounds.MaxLat)
}
}
Viewport-Based Loading
Complete, compilable version: docs/examples/viewport-loading/main.go
Load charts appropriate for a viewport and zoom level:
func loadForViewport(mgr *chartmanager.ChartManager) {
// User's current viewport
viewport := s57.Bounds{
MinLon: -122.45, MaxLon: -122.35,
MinLat: 37.75, MaxLat: 37.85,
}
// Zoom level (12 = coastal charts)
zoom := 12
// Load appropriate charts (auto-downloads if needed)
charts, err := mgr.GetChartsForViewport(viewport, zoom)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Loaded %d charts for zoom %d\n", len(charts), zoom)
// Process features across all charts
allFeatures := make([]s57.Feature, 0)
for _, chart := range charts {
features := chart.FeaturesInBounds(viewport)
allFeatures = append(allFeatures, features...)
fmt.Printf(" %s: %d features\n",
chart.DatasetName(), len(features))
}
fmt.Printf("Total features in viewport: %d\n", len(allFeatures))
}
Specific Chart Loading
Load a specific chart by name:
func loadSpecificChart(mgr *chartmanager.ChartManager) {
// Load Boston Harbor chart
chart, err := mgr.GetChart("US5MA22M")
if err != nil {
log.Fatal(err)
}
// Print chart information
fmt.Printf("Chart: %s\n", chart.DatasetName())
fmt.Printf("Edition: %s\n", chart.Edition())
fmt.Printf("Update: %s\n", chart.UpdateNumber())
fmt.Printf("Features: %d\n", chart.FeatureCount())
// Get chart bounds
bounds := chart.Bounds()
fmt.Printf("Coverage: [%.4f,%.4f] to [%.4f,%.4f]\n",
bounds.MinLon, bounds.MinLat,
bounds.MaxLon, bounds.MaxLat)
// Query features in a specific area
searchArea := s57.Bounds{
MinLon: -71.05, MaxLon: -71.04,
MinLat: 42.35, MaxLat: 42.36,
}
features := chart.FeaturesInBounds(searchArea)
fmt.Printf("Features in search area: %d\n", len(features))
}
Multi-Chart Queries
Work with multiple charts efficiently:
func searchMultipleCharts(mgr *chartmanager.ChartManager) {
// Define large search area (New York Harbor)
searchArea := s57.Bounds{
MinLon: -74.1, MaxLon: -73.9,
MinLat: 40.6, MaxLat: 40.8,
}
// Load all appropriate charts
charts, err := mgr.GetChartsForViewport(searchArea, 14)
if err != nil {
log.Fatal(err)
}
// Search for specific feature types across all charts
var wrecks []s57.Feature
for _, chart := range charts {
features := chart.FeaturesInBounds(searchArea)
for _, f := range features {
if f.ObjectClass == "WRECKS" {
wrecks = append(wrecks, f)
}
}
}
fmt.Printf("Found %d wrecks in New York Harbor\n", len(wrecks))
// Print details
for i, wreck := range wrecks {
fmt.Printf("\nWreck %d:\n", i+1)
if name, ok := wreck.Attributes["OBJNAM"].(string); ok {
fmt.Printf(" Name: %s\n", name)
}
if depth, ok := wreck.Attributes["VALSOU"].(float64); ok {
fmt.Printf(" Depth: %.1f meters\n", depth)
}
geom := wreck.Geometry
if geom.Type == s57.GeometryTypePoint {
lon, lat := geom.Coordinates[0][0], geom.Coordinates[0][1]
fmt.Printf(" Position: %.6f, %.6f\n", lon, lat)
}
}
}
Custom Configuration
Configure the chart manager for different use cases:
func customConfiguration() {
// Low-memory embedded system
lowMemoryOpts := chartmanager.ChartManagerOptions{
CacheSize: 64 * 1024 * 1024, // 64MB cache
KeepExtracted: false, // Stream from zips
MaxCatalogAge: 30 * 24 * time.Hour, // Monthly updates
}
// High-performance server
highPerfOpts := chartmanager.ChartManagerOptions{
CacheSize: 2 * 1024 * 1024 * 1024, // 2GB cache
KeepExtracted: true, // Extract to disk
MaxCatalogAge: 24 * time.Hour, // Daily updates
}
// Development/testing with custom paths
devOpts := chartmanager.ChartManagerOptions{
CacheSize: 512 * 1024 * 1024,
CatalogPath: "/tmp/test-catalog.xml",
ChartCacheDir: "/tmp/test-charts",
ForceUpdate: true, // Always fresh catalog
}
mgr, err := chartmanager.NewChartManager(lowMemoryOpts)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Manager initialized with custom config\n")
stats := mgr.Stats()
fmt.Printf("Max memory: %.1f MB\n",
float64(stats.MaxMemory)/(1024*1024))
}
Monitoring and Statistics
Track performance and cache effectiveness:
func monitorPerformance(mgr *chartmanager.ChartManager) {
// Initial stats
stats := mgr.Stats()
fmt.Printf("=== Chart Manager Statistics ===\n")
fmt.Printf("Catalog Charts: %d\n", stats.CatalogCharts)
fmt.Printf("Indexed Charts: %d\n", stats.IndexedCharts)
fmt.Printf("Cached Charts: %d\n", stats.CachedCharts)
fmt.Printf("\n=== Cache Performance ===\n")
fmt.Printf("Hits: %d\n", stats.CacheHits)
fmt.Printf("Misses: %d\n", stats.CacheMisses)
fmt.Printf("Hit Rate: %.1f%%\n", stats.CacheHitRate()*100)
fmt.Printf("\n=== Memory Usage ===\n")
fmt.Printf("Current: %.1f MB\n",
float64(stats.CacheMemory)/(1024*1024))
fmt.Printf("Maximum: %.1f MB\n",
float64(stats.MaxMemory)/(1024*1024))
fmt.Printf("Usage: %.1f%%\n",
float64(stats.CacheMemory)/float64(stats.MaxMemory)*100)
fmt.Printf("\n=== Paths ===\n")
fmt.Printf("Catalog: %s\n", stats.CatalogPath)
fmt.Printf("Cache: %s\n", stats.ChartCacheDir)
}
Catalog Updates
Manage catalog freshness:
func manageCatalog(mgr *chartmanager.ChartManager) {
// Check catalog age
stats := mgr.Stats()
catalogPath := stats.CatalogPath
info, err := os.Stat(catalogPath)
if err == nil {
age := time.Since(info.ModTime())
fmt.Printf("Catalog age: %v\n", age)
// Force update if old
if age > 7*24*time.Hour {
fmt.Println("Catalog is old, updating...")
if err := mgr.UpdateCatalog(); err != nil {
log.Printf("Update failed: %v", err)
} else {
fmt.Println("Catalog updated successfully")
}
}
}
// Check chart count after update
fmt.Printf("Charts available: %d\n", mgr.ChartCount())
}
Region Queries
Query by predefined regions:
func queryRegions(mgr *chartmanager.ChartManager) {
regions := map[string]s57.Bounds{
"San Francisco Bay": {
MinLon: -122.5, MaxLon: -122.0,
MinLat: 37.5, MaxLat: 38.0,
},
"New York Harbor": {
MinLon: -74.1, MaxLon: -73.9,
MinLat: 40.6, MaxLat: 40.8,
},
"Chesapeake Bay": {
MinLon: -76.6, MaxLon: -76.0,
MinLat: 38.5, MaxLat: 39.5,
},
}
for name, bounds := range regions {
entries := mgr.QueryCatalog(bounds)
var totalSize float64
for _, entry := range entries {
totalSize += entry.SizeMB
}
fmt.Printf("%s:\n", name)
fmt.Printf(" Charts: %d\n", len(entries))
fmt.Printf(" Total size: %.1f MB\n\n", totalSize)
}
}
Complete Example: Chart Server
Complete, compilable version: docs/examples/chart-server/main.go
A simple HTTP server that serves chart data:
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strconv"
"github.com/beetlebugorg/chartmanager/pkg/chartmanager"
"github.com/beetlebugorg/s57/pkg/s57"
)
var mgr *chartmanager.ChartManager
func main() {
// Initialize chart manager
var err error
mgr, err = chartmanager.NewChartManager(
chartmanager.DefaultChartManagerOptions(),
)
if err != nil {
log.Fatal(err)
}
log.Printf("Chart server ready: %d charts indexed", mgr.ChartCount())
// Routes
http.HandleFunc("/charts", handleChartList)
http.HandleFunc("/features", handleFeatures)
http.HandleFunc("/stats", handleStats)
log.Println("Listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
// GET /charts?minLon=...&maxLon=...&minLat=...&maxLat=...
func handleChartList(w http.ResponseWriter, r *http.Request) {
bounds := s57.Bounds{
MinLon: parseFloat(r.URL.Query().Get("minLon")),
MaxLon: parseFloat(r.URL.Query().Get("maxLon")),
MinLat: parseFloat(r.URL.Query().Get("minLat")),
MaxLat: parseFloat(r.URL.Query().Get("maxLat")),
}
entries := mgr.QueryCatalog(bounds)
json.NewEncoder(w).Encode(entries)
}
// GET /features?minLon=...&maxLon=...&minLat=...&maxLat=...&zoom=12
func handleFeatures(w http.ResponseWriter, r *http.Request) {
bounds := s57.Bounds{
MinLon: parseFloat(r.URL.Query().Get("minLon")),
MaxLon: parseFloat(r.URL.Query().Get("maxLon")),
MinLat: parseFloat(r.URL.Query().Get("minLat")),
MaxLat: parseFloat(r.URL.Query().Get("maxLat")),
}
zoom := parseInt(r.URL.Query().Get("zoom"))
charts, err := mgr.GetChartsForViewport(bounds, zoom)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Collect features from all charts
allFeatures := []s57.Feature{}
for _, chart := range charts {
features := chart.FeaturesInBounds(bounds)
allFeatures = append(allFeatures, features...)
}
json.NewEncoder(w).Encode(allFeatures)
}
// GET /stats
func handleStats(w http.ResponseWriter, r *http.Request) {
stats := mgr.Stats()
json.NewEncoder(w).Encode(stats)
}
func parseFloat(s string) float64 {
f, _ := strconv.ParseFloat(s, 64)
return f
}
func parseInt(s string) int {
i, _ := strconv.Atoi(s)
return i
}
Usage:
# Start server
go run server.go
# Query charts
curl "http://localhost:8080/charts?minLon=-122.5&maxLon=-122.0&minLat=37.5&maxLat=38.0"
# Get features
curl "http://localhost:8080/features?minLon=-122.45&maxLon=-122.35&minLat=37.75&maxLat=37.85&zoom=12"
# Check stats
curl "http://localhost:8080/stats"