- Go 100%
Thread a context.Context through every HTTP request via a request() helper bound with SetContext. WithContext sets it at construction; a canceled context now aborts in-flight requests and the flexigrid page loops stop dispatching further pages. This lets callers (e.g. nidus-sync MapVisionSyncRun) cancel a long sync on shutdown instead of waiting for every page to complete. |
||
|---|---|---|
| cmd | ||
| db | ||
| .gitignore | ||
| access_group.go | ||
| action.go | ||
| adult_trap.go | ||
| carcass.go | ||
| client.go | ||
| contact.go | ||
| container_type.go | ||
| dashboard.go | ||
| flexigrid.go | ||
| flexigrid_test.go | ||
| go.mod | ||
| go.sum | ||
| inv_item_lot.go | ||
| inv_location.go | ||
| inv_material.go | ||
| larval_sample.go | ||
| manage_pool_tests.go | ||
| map_cache.go | ||
| map_cache_test.go | ||
| map_export.go | ||
| map_export_test.go | ||
| map_preferences.go | ||
| map_preferences_test.go | ||
| master_lists.go | ||
| my_task.go | ||
| operation_treatment.go | ||
| operation_treatment_test.go | ||
| pip.go | ||
| public_sr.go | ||
| README.md | ||
| service_request.go | ||
| site.go | ||
| site_block.go | ||
| treatment_plan.go | ||
| user.go | ||
| wnv_response_log.go | ||
| workflow.go | ||
MapVision Go
A Go client library and CLI for MapVision Enterprise, a mosquito and vector control management platform. Since MapVision has no public API, this project scrapes data by authenticating via its HTML login flow and then either parsing server-rendered HTML pages or POSTing to the JSON endpoints that power its flexigrid data tables.
Installation
go install source.gleipnir.technology/Gleipnir/mapvision-go/cmd/mapvision@latest
Or build from source:
git clone https://source.gleipnir.technology/Gleipnir/mapvision-go
cd mapvision-go
go build -o mapvision ./cmd/mapvision
Quick Start
# Store credentials (saved to ~/.config/mapvision/config.json)
mapvision login -username myuser -password mypass
# List service requests from the My Task page
mapvision my_task sr list
# List all users (paginated)
mapvision user list --per-page 50
# Search sites by name
mapvision site list --search "Roseville"
Authentication
All data-fetching commands require login. Credentials are resolved in this order:
- CLI flags
-username/-password - Environment variables
MAPVISION_USERNAME/MAPVISION_PASSWORD - Saved config file at
~/.config/mapvision/config.json(respects$XDG_CONFIG_HOME)
Only mapvision login writes credentials to disk (pass --dry-run to skip the save). Every other command uses the credentials you supply for that invocation and never saves them.
mapvision login -username myuser -password mypass
The client stores session cookies in an in-memory cookie jar, so a single Login() call authenticates all subsequent requests.
CLI Commands
Core
| Command | Description |
|---|---|
login |
Authenticate with MapVision |
password-reset |
Request password reset for a username |
Data Lists (all support list action with --page, --per-page, --sort, --order, --search)
| Command | MapVision Page |
|---|---|
user |
View Users |
contact |
View Contacts |
access_group |
Access Groups |
site |
View Sites |
site_block |
View Site Block |
site_block_adv |
View Site Block (Advanced) |
workflow |
Workflow List |
action |
Activity Cards |
service_request |
Browse Service Requests |
public_sr |
Pending Public Queue |
inv_material |
Material Master |
inv_item |
Item Master (same data as material) |
inv_item_lot |
Inventory Item Lot |
inv_location |
Inventory Location |
wnv_response_log |
WNV Response Log |
larval_sample |
Manage Larval Samples |
adult_trap |
Adult Surveillance/Traps |
pool_test |
Manage Pools and Tests |
Map Data Export (GeoJSON)
| Command | Description |
|---|---|
zones |
List the zones visible to the logged-in account (IDs feed map_export -zones) |
map_export |
Export all map layers as GeoJSON FeatureCollections |
The map_export command POSTs to POST /map/technician (the same endpoint the browser-side Google Maps JS uses) with a mode parameter for each data layer. It produces RFC 7946 GeoJSON files with properties attached to each feature.
# Export all layers as individual GeoJSON files
mapvision map_export -username myuser -password mypass
# Export specific zones, only open & closed SRs, WNV data for the past month
mapvision map_export -zones 68,63,64 -sr-status 2,3 -carcass-period month -mosquito-period month
# Write a single combined GeoJSON file
mapvision map_export --combined -out-dir ./export
| Flag | Default | Description |
|---|---|---|
-zones |
all zones from map preferences | Comma-separated zone IDs (e.g. 68,63,64) |
-property |
unset (no filter) | Service provider ID (iSPId). Unset by default so the request returns all sites, matching the browser; setting it filters to that provider. |
-sr-status |
all statuses | SR status IDs: 2=Open, 3=Closed, 7=Review, 8=Reinspect |
-carcass-period |
week |
WNV carcass time window: week, month, quarter, year |
-mosquito-period |
week |
WNV mosquito time window: week, month, quarter, year |
-out-dir |
. |
Output directory for GeoJSON files |
-combined |
false |
Write a single map_export.geojson instead of per-layer files |
-inactive-sites |
false |
Send InactiveSites=1 (include inactive sites) |
-layer |
all layers | Comma-separated layers to export: zones, sites, catch_basins, service_requests, site_blocks, treatment_plans, traps, wnv_carcasses, wnv_mosquito_pools. Useful to test one asset type at a time. |
-dump-raw |
— | Write the raw /map/technician response JSON to a path and exit (diagnostic). Inspect with jq to see exactly which layers the server returns. |
-dump-mode |
zones |
The mode to fetch when -dump-raw is set (e.g. catch_basins, treatment_plan). |
# Export only traps (useful for testing one asset type)
mapvision map_export -layer traps -out-dir ./export
# Dump the raw catch_basins response to inspect its shape
mapvision map_export -dump-raw ./cb.json -dump-mode catch_basins
Output Files (per-layer mode)
| File | Geometry Type | Contents |
|---|---|---|
zones.geojson |
Polygon | Zone boundary polygons with iZoneId, vZoneName, vDistrict |
sites.geojson |
Point | Site markers from the map's rs_site payload with lat/lng, iSiteId, vName, vTypeName, etc. |
catch_basins.geojson |
Polygon | Catch-basin subzone grid polygons with iZoneId, iSZoneId, vSubZone |
service_requests.geojson |
Point | SR markers with iSRId, vStatus, sr_type, contact info |
site_blocks.geojson |
Polygon | Site block polygons with iBlockId, vBlockName, rArea |
treatment_plans.geojson |
Polygon | Treatment plan polygons with iTreatmentPlanId, vPlanName, vScheduledActivity |
traps.geojson |
Point | Trap locations with iATTId, vTrapName, dTrapPlaced, dTrapCollected |
wnv_carcasses.geojson |
Point | WNV-positive carcass markers with iWRLId, tDescription, radius meters |
wnv_mosquito_pools.geojson |
Point | WNV-positive mosquito pool markers with iATPId, SpecieName, vPoolNo |
Library Usage
params := mapvision.DefaultMapExportParams()
params.ZoneIDs = []int{68, 63}
params.CarcassPastMonth = true
result, err := client.MapExport(params)
// Access individual layers
for _, f := range result.Sites.Features {
fmt.Printf("Site %s at %v\n",
f.Properties["iSiteId"],
f.Geometry.Coordinates,
)
}
PostGIS Storage
The exported layers can be loaded into a PostGIS database. db/schema.sql
creates one table per layer (all geometries in SRID 4326) with a GIST spatial
index and an idempotent natural-key; db/load.sql streams the exported
.geojson files in.
# Requires a PostgreSQL server with the postgis extension
psql -d mapvision -f db/schema.sql
# From the directory containing the exported *.geojson files
cd ./export
psql -d mapvision -f /path/to/mapvision-go/db/load.sql
db/load.sql uses jq to flatten each FeatureCollection to one feature per
line and \copy ... FROM PROGRAM to stream them (handles the ~50MB
sites.geojson), then upserts. Note two quirks it handles for you:
- PostgreSQL
COPYinterprets backslash escapes, so a literal\tin a site name would otherwise corrupt the row; the loader doubles backslashes first. site_blocks.i_block_id/i_site_idcan be comma-separated lists, and atreatment_plans.i_treatment_plan_idcan span multiple polygons, so those two tables have no unique key and are truncated on reload.
| Table | Layer | Geometry |
|---|---|---|
zones |
zone boundaries | Polygon |
sites |
site points | Point |
catch_basins |
subzone grid polygons | Polygon |
service_requests |
SR markers | Point |
site_blocks |
block polygons | Polygon |
treatment_plans |
plan polygons | Polygon |
traps |
trap locations | Point |
wnv_carcasses |
WNV carcasses | Point |
wnv_mosquito_pools |
WNV pools | Point |
Map API
MapVision exposes the interactive map as a single endpoint:
POST /map/technician
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Every layer is a mode=<layer> in the form body; a successful response is a
JSON object whose keys are the layer payload arrays (e.g. rs_zone,
rs_site, rs_at, rs_sr). The map page's filter panel renders as
checkboxes whose name/value map directly onto the form fields, so the
browser's request body is the authoritative parameter list.
Zone context dependency. Most layers return nothing unless the zone
boundaries/sites request has been issued for the current selection. The
browser fires mode=zones (the large payload, ~100 MB when all zones are
selected) whenever a layer is toggled, and the zone/subzone selectors it
carries gate the other layers.
Layer → request mapping.
| Layer | Body |
|---|---|
| zones + sites | mode=zones&iZoneId=…&iSZoneId=…&gear_siteid=undefined&siterow=0&bNoSpray=…&bNotify=…&InactiveSites=… |
| catch basins | mode=catch_basins&iSZoneId=… |
| service requests | mode=sr&iZoneId=…&iSRSMId=2,7,3,8 |
| site blocks | mode=site_block&all_blocks=1&site_blocks=0&recent_site_block=0&exclude_site_block=0 |
| treatment plans | mode=treatment_plan |
| traps | mode=trap&… |
| WNV carcasses | mode=positive_wmv_carcas&wmv_past_week=…&wmv_year_date=0&iSRRRId= |
| WNV mosquito pools | mode=positive_pool_mosquitos&mos_past_week=…&mos_year_date=0&iATPDId= |
Filters. The checkbox-derived selectors, and what they do:
iZoneId/iSZoneId— zone andzone_subzone(e.g.68_5) selectors. Subzones are required to enumerate every site; without them the server returns only a single default zone.iSTypeId/iSSTypeId— parent site-type and sub-type IDs. Send the full list (all types) or the server returns almost nothing. Sub-type values arrive as raw checkbox values, where "Unknown" rows carry0||<parent>— that's why theiSSTypeIdfield is||-delimited.iFCodeId— hazard-flag filter. Sending all hazard flags narrows sites to only those carrying a hazard (e.g. 63,805 → ~5,814).iSPId/iSPId_arr— service-provider filter. Leave empty for the full site catalog; setting it restricts to that provider (this was the gate that silently returned 4 sites).iSRSMId— SR status IDs (2=Open,7=Review,3=Closed,8=Reinspect). Omit it andmode=srreturns no SRs.bNoSpray,bNotify,InactiveSites— boolean toggles.mode=site_block&all_blocks=0&site_blocks=0&recent_site_block=0&exclude_site_block=0is the no-op the browser fires when deselecting a layer; it returns{"rs_blocks_poly":[]}.
Response shapes. mode=zones returns rs_zone (each zone embeds its
sites array — the bulk of the payload) plus a small top-level rs_site.
The dedicated modes return their own arrays: rs_sr, rs_blocks_poly,
rs_treatment_plan_poly, rs_at (traps), rs_carcass,
rs_tp_details (pools), and rs_subzone (catch-basin grid polygons).
Field quirks. Geometry comes in two formats: PShape as
lng lat, lng lat pairs (zones, catch basins) and PShape1 as
((lng,lat),(lng,lat)) WKT (site blocks, treatment plans). rRadius_meter
and the SR action field may arrive as a JSON number or string, and the
hazard-flag checkboxes repeat a trailing value="" attribute — all handled
by the exporter.
HTML-Scraped Pages (table subcommand required)
| Command | Tables |
|---|---|
my_task |
sr, ls/lab_surveillance, wf/workflow, pip/parcel_inspection |
dashboard |
sra/sr_assigned, pdb/positive_dead_birds, ann/announcements, pmp/positive_mosquito_pools, sr/service_requests |
Library Usage
import mapvision "source.gleipnir.technology/Gleipnir/mapvision-go"
func main() {
client := mapvision.NewClient("https://placer.leateamapps.com",
mapvision.WithDebug(true),
)
// Authenticate
if _, err := client.Login("username", "password"); err != nil {
log.Fatal(err)
}
// Flexigrid-backed list
users, _ := client.UserList(mapvision.DefaultUserListParams())
for _, u := range users.Items {
fmt.Printf("%s: %s <%s>\n", u.ID, u.Name, u.Email)
}
// Or fetch the full my_task HTML page and parse all sections
page, _ := client.MyTask()
for _, sr := range page.ServiceRequests {
fmt.Printf("SR #%s: %s (%s)\n", sr.SRID, sr.Status, sr.SRType)
}
}
Downloading the map to a file cache
Client.DownloadMapCache fetches all geographic layers over the network once,
writes each to a GeoJSON file in a local directory, and returns a MapCache
whose features are streamed back through a Go iterator — so the ~50 MB
sites.geojson is never held in memory at once. Subsequent reads reuse the
files via OpenMapCache with no network access.
import mapvision "source.gleipnir.technology/Gleipnir/mapvision-go"
// client is the logged-in client from the example above.
// 1. Download every layer into ./cache (hits the network; ~2 min for all zones).
cache, err := client.DownloadMapCache(mapvision.DefaultMapExportParams(), "./cache")
if err != nil {
log.Fatal(err)
}
// 2. Iterate a cached layer — streams features from disk, no network.
for feat := range cache.Features(mapvision.LayerSites) {
fmt.Printf("site %v at %v\n",
feat.Properties["iSiteId"],
feat.Geometry.Coordinates,
)
}
fmt.Printf("total sites: %d\n", cache.Count(mapvision.LayerSites))
for _, layer := range cache.LayerNames() {
fmt.Printf("cached %s: %d features\n", layer, cache.Count(layer))
}
// 3. Later (new process), reopen the cache without re-downloading.
cache = mapvision.OpenMapCache("./cache")
for feat := range cache.Features(mapvision.LayerTraps) {
fmt.Printf("trap %v\n", feat.Properties["iATTId"])
}
DefaultMapExportParams() downloads the full catalog (all zones, subzones, and
site types for the account). To cache a subset, set params.Layers — e.g.
params.Layers = []string{"sites", "traps"} — or pin params.ZoneIDs.
Architecture
The project has two packages:
mapvision — Library package (root directory)
client.go— HTTP client with cookie jar,Login(),MyTask(),Dashboard(), and all list methodsflexigrid.go— Generic pagination engine for flexigrid JSON endpointsmy_task.go/dashboard.go— HTML DOM parsers usinggolang.org/x/net/html- One file per MapVision entity (e.g.,
user.go,site.go,workflow.go) — struct definition + list method map_export.go— Map export client methods, WKT polygon parser, GeoJSON serialization for all map layers (zones, sites, SRs, blocks, treatment plans, traps, WNV markers)
cmd/mapvision — CLI binary
main.go— Subcommand dispatch, tabular output usingtext/tabwriter, credential resolutionconfig.go— XDG config file read/write for persistent credentials
Patterns
Flexigrid List Pattern
Most MapVision data tables are powered by the flexigrid jQuery plugin. The JS sends a POST with form-encoded pagination params (pg, rp, sortname, sortorder, query, qtype) to ?mode=List and receives a JSON response:
{
"page": 1,
"total": 42,
"rows": [
{ "id": "335", "cell": ["335", "Nidus", "nidus@example.com", ...] }
]
}
The cell value can be either a positional array or a keyed object. The generic FlexigridList[T]() function handles both:
func FlexigridList[T any](
c *Client,
endpoint string,
params FlexigridListParams,
defaultSortName string,
defaultSortOrder string,
mapRow func(rowID string, cells []string, obj map[string]string) T,
) (*FlexigridListResult[T], error)
Each entity file defines a struct, a row mapper, and a thin public method:
type Entity struct { ... }
func (c *Client) EntityList(params EntityListParams) (*EntityListResult, error) {
return FlexigridList(c, "/path?mode=List", params, "defaultSort", "DESC", entityRowMapper)
}
HTML Page Parsing
The My Task and Dashboard pages have no JSON endpoints — the server renders full HTML pages. These are parsed into typed structs using golang.org/x/net/html:
- Table rows are extracted by DOM ID traversal (
findByID()→findRows()→findCells()) - Priority colors are parsed from inline
style="background-color:#XXXXXX"attributes <br>tags in cell text are preserved as newlines- Dashboard alert boxes are located by heading text (e.g., "SR Assigned Alert") and navigated upward to the parent
.alertboxdiv
Credential Storage
Credentials are stored as JSON at ~/.config/mapvision/config.json (respects $XDG_CONFIG_HOME). The config file has 0600 permissions. Only mapvision login writes credentials to disk — pass --dry-run to skip the save. All other commands resolve credentials from flags, then MAPVISION_USERNAME/MAPVISION_PASSWORD, then the saved config, and never write them back.
Adding a New Flexigrid Entity
- Create a new file in the root package (e.g.,
new_entity.go) - Define a struct with JSON tags matching the flexigrid
colModel - Create type aliases:
NewEntityListParams = FlexigridListParams,NewEntityListResult = FlexigridListResult[NewEntity] - Write a
NewEntityList()method onClientthat callsFlexigridList() - Write a
newEntityRowMapper()that populates the struct from either the object map or positional cells - Add a CLI handler in
cmd/mapvision/main.go: switch case, usage entry,handleNewEntity(),displayNewEntities()