Example #1
0
// Serves API documentation.  Route requests for /api-docs* to this handler.
func (d *Docs) Serve(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", web.HtmlContent)
	w.Header().Set("Surrogate-Control", web.MaxAge300)
	switch {
	case r.URL.Path == "/api-docs" || r.URL.Path == "/api-docs/" || r.URL.Path == "/api-docs/index.html":
		b, err := d.indexPage()
		if err != nil {
			web.ServiceUnavailablePage(w, r, err)
			return
		}
		web.OkBuf(w, r, b)
	// /api-docs/endpoints/
	case strings.HasPrefix(r.URL.Path, endpointPath):
		if _, ok := d.endpoints[r.URL.Path[endpointsLen:]]; !ok {
			web.NotFoundPage(w, r)
			return
		}
		b, err := d.endpointPage(r.URL.Path[endpointsLen:])
		if err != nil {
			web.ServiceUnavailablePage(w, r, err)
			return
		}
		web.OkBuf(w, r, b)
	default:
		web.NotFoundPage(w, r)
	}
}
Example #2
0
// returns a simple state of health page.  If heartbeat times in the DB are old then it also returns an http status of 500.
func soh(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", web.HtmlContent)
	var b bytes.Buffer

	b.Write([]byte(head))
	b.Write([]byte(`<p>Current time is: ` + time.Now().UTC().String() + `</p>`))
	b.Write([]byte(`<h3>Messaging</h3>`))

	var bad bool
	var s string
	var t time.Time

	b.Write([]byte(`<table><tr><th>Service</th><th>Time Received</th></tr>`))

	rows, err := db.Query("select serverid, timereceived from qrt.soh")
	if err == nil {
		defer rows.Close()
		for rows.Next() {
			err := rows.Scan(&s, &t)
			if err == nil {
				if t.Before(time.Now().UTC().Add(old)) {
					bad = true
					b.Write([]byte(`<tr class="tr error">`))
				} else {
					b.Write([]byte(`<tr>`))
				}
				b.Write([]byte(`<td>` + s + `</td><td>` + t.String() + `</td></tr>`))
			} else {
				bad = true
				b.Write([]byte(`<tr class="tr error"><td>DB error</td><td>` + err.Error() + `</td></tr>`))
			}
		}
		rows.Close()
	} else {
		bad = true
		b.Write([]byte(`<tr class="tr error"><td>DB error</td><td>` + err.Error() + `</td></tr>`))
	}
	b.Write([]byte(`</table>`))

	b.Write([]byte(foot))

	if bad {
		web.ServiceInternalServerErrorBuf(w, r, &b)
		return
	}

	web.OkBuf(w, r, &b)
}
Example #3
0
// returns a simple state of health page.  If the count of measured intensities falls below 50 this it also returns an http status of 500.
func impactSOH(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", web.HtmlContent)
	var b bytes.Buffer

	b.Write([]byte(head))
	b.Write([]byte(`<p>Current time is: ` + time.Now().UTC().String() + `</p>`))
	b.Write([]byte(`<h3>Impact</h3>`))

	var bad bool

	b.Write([]byte(`<table><tr><th>Impact</th><th>Count</th></tr>`))

	var meas int
	err := db.QueryRow("select count(*) from impact.intensity_measured").Scan(&meas)
	if err == nil {
		if meas < 50 {
			bad = true
			b.Write([]byte(`<tr class="tr error"><td>shaking measured</td><td>` + strconv.Itoa(meas) + ` < 50</td></tr>`))
		} else {
			b.Write([]byte(`<tr><td>shaking measured</td><td>` + strconv.Itoa(meas) + ` >= 50</td></tr>`))
		}
	} else {
		bad = true
		b.Write([]byte(`<tr class="tr error"><td>DB error</td><td>` + err.Error() + `</td></tr>`))
	}
	b.Write([]byte(`</table>`))

	b.Write([]byte(foot))

	if bad {
		web.ServiceInternalServerErrorBuf(w, r, &b)
		return
	}

	web.OkBuf(w, r, &b)
}
Example #4
0
func spark(w http.ResponseWriter, r *http.Request) {
	if err := sparkD.CheckParams(r.URL.Query()); err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	var plotType string
	var s siteQ
	var t typeQ
	var days int
	var ymin, ymax float64
	var stddev string
	var label string
	var ok bool

	if plotType, ok = getPlotType(w, r); !ok {
		return
	}

	if stddev, ok = getStddev(w, r); !ok {
		return
	}

	if label, ok = getSparkLabel(w, r); !ok {
		return
	}

	if days, ok = getDays(w, r); !ok {
		return
	}

	if ymin, ymax, ok = getYRange(w, r); !ok {
		return
	}

	if t, ok = getType(w, r); !ok {
		return
	}

	if s, ok = getSite(w, r); !ok {
		return
	}

	var p plt
	var tmin time.Time

	if days > 0 {
		n := time.Now().UTC()
		tmin = n.Add(time.Duration(days*-1) * time.Hour * 24)
		p.SetXAxis(tmin, n)
		days = 0 // add all data > than tmin
	}

	switch {
	case ymin == 0 && ymax == 0:
	case ymin == ymax:
		p.SetYRange(ymin)
	default:
		p.SetYAxis(ymin, ymax)
	}

	p.SetUnit(t.unit)

	var err error

	if stddev == `pop` {
		err = p.setStddevPop(s, t, tmin, days)
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	err = p.addSeries(t, tmin, days, s)
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	b := new(bytes.Buffer)

	switch plotType {
	case ``, `line`:
		switch label {
		case ``, `all`:
			err = ts.SparkLineAll.Draw(p.Plot, b)
		case `latest`:
			err = ts.SparkLineLatest.Draw(p.Plot, b)
		case `none`:
			err = ts.SparkLineNone.Draw(p.Plot, b)
		}
	case `scatter`:
		switch label {
		case ``, `all`:
			err = ts.SparkScatterAll.Draw(p.Plot, b)
		case `latest`:
			err = ts.SparkScatterLatest.Draw(p.Plot, b)
		case `none`:
			err = ts.SparkScatterNone.Draw(p.Plot, b)
		}
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	w.Header().Set("Content-Type", "image/svg+xml")
	web.OkBuf(w, r, b)
}
Example #5
0
func plotSite(w http.ResponseWriter, r *http.Request) {
	if err := plotSiteD.CheckParams(r.URL.Query()); err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	var plotType string
	var s siteQ
	var t typeQ
	var start time.Time
	var days int
	var ymin, ymax float64
	var showMethod bool
	var stddev string
	var ok bool

	if plotType, ok = getPlotType(w, r); !ok {
		return
	}

	if showMethod, ok = getShowMethod(w, r); !ok {
		return
	}

	if stddev, ok = getStddev(w, r); !ok {
		return
	}

	if start, ok = getStart(w, r); !ok {
		return
	}

	if days, ok = getDays(w, r); !ok {
		return
	}

	if ymin, ymax, ok = getYRange(w, r); !ok {
		return
	}

	if t, ok = getType(w, r); !ok {
		return
	}

	if s, ok = getSite(w, r); !ok {
		return
	}

	var p plt

	switch {
	case start.IsZero() && days == 0:
		// do nothing - autorange on the data.
	case start.IsZero() && days > 0:
		n := time.Now().UTC()
		start = n.Add(time.Duration(days*-1) * time.Hour * 24)
		p.SetXAxis(start, n)
		days = 0 // add all data > than start by setting 0.  Allows for adding start end to URL.
	case !start.IsZero() && days > 0:
		p.SetXAxis(start, start.Add(time.Duration(days*1)*time.Hour*24))
	case !start.IsZero() && days == 0:
		web.BadRequest(w, r, "Invalid start specified without days")
		return
	}

	switch {
	case ymin == 0 && ymax == 0:
	case ymin == ymax:
		p.SetYRange(ymin)
	default:
		p.SetYAxis(ymin, ymax)
	}

	p.SetTitle(fmt.Sprintf("%s (%s) - %s", s.siteID, s.name, t.description))
	p.SetUnit(t.unit)
	p.SetYLabel(fmt.Sprintf("%s (%s)", t.name, t.unit))

	var err error

	switch showMethod {
	case false:
		err = p.addSeries(t, start, days, s)
	case true:
		err = p.addSeriesLabelMethod(t, start, days, s)
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	if stddev == `pop` {
		err = p.setStddevPop(s, t, start, days)
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	b := new(bytes.Buffer)

	switch plotType {
	case ``, `line`:
		err = ts.Line.Draw(p.Plot, b)
	case `scatter`:
		err = ts.Scatter.Draw(p.Plot, b)
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	w.Header().Set("Content-Type", "image/svg+xml")
	web.OkBuf(w, r, b)
}
Example #6
0
func observation(w http.ResponseWriter, r *http.Request) {
	if err := observationD.CheckParams(r.URL.Query()); err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	v := r.URL.Query()

	typeID := v.Get("typeID")
	networkID := v.Get("networkID")
	siteID := v.Get("siteID")

	if !validType(w, r, typeID) {
		return
	}

	var days int

	if v.Get("days") != "" {
		var err error
		days, err = strconv.Atoi(v.Get("days"))
		if err != nil || days > 365000 {
			web.BadRequest(w, r, "Invalid days query param.")
			return
		}
	}

	var methodID string

	if v.Get("methodID") != "" {
		methodID = v.Get("methodID")
		if !validTypeMethod(w, r, typeID, methodID) {
			return
		}
	}

	// Find the unit for the CSV header
	var unit string
	err := db.QueryRow("select symbol FROM fits.type join fits.unit using (unitPK) where typeID = $1", typeID).Scan(&unit)
	if err == sql.ErrNoRows {
		web.NotFound(w, r, "unit not found for typeID: "+typeID)
		return
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	var d string
	var rows *sql.Rows

	switch {
	case days == 0 && methodID == "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s', to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) as csv FROM fits.observation 
                           WHERE 
                               sitepk = (
                                              SELECT DISTINCT ON (sitepk) sitepk from fits.site join fits.network using (networkpk) where siteid = $2 and networkid = $1 
                                            )
                               AND typepk = (
                                                        SELECT typepk FROM fits.type WHERE typeid = $3
                                                       ) 
                                 ORDER BY time ASC;`, networkID, siteID, typeID)
	case days != 0 && methodID == "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s', to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) as csv FROM fits.observation 
                           WHERE 
                               sitepk = (
                                              SELECT DISTINCT ON (sitepk) sitepk from fits.site join fits.network using (networkpk) where siteid = $2 and networkid = $1 
                                            )
                               AND typepk = (
                                                        SELECT typepk FROM fits.type WHERE typeid = $3
                                                       ) 
                                AND time > (now() - interval '`+strconv.Itoa(days)+` days')
                  		ORDER BY time ASC;`, networkID, siteID, typeID)
	case days == 0 && methodID != "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s', to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) as csv FROM fits.observation 
                           WHERE 
                               sitepk = (
                                              SELECT DISTINCT ON (sitepk) sitepk from fits.site join fits.network using (networkpk) where siteid = $2 and networkid = $1 
                                            )
                               AND typepk = (
                                                         SELECT typepk FROM fits.type WHERE typeid = $3
                                                       ) 
			AND methodpk = (
					SELECT methodpk FROM fits.method WHERE methodid = $4
				)
                                 ORDER BY time ASC;`, networkID, siteID, typeID, methodID)
	case days != 0 && methodID != "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s', to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) as csv FROM fits.observation 
                           WHERE 
                               sitepk = (
                                              SELECT DISTINCT ON (sitepk) sitepk from fits.site join fits.network using (networkpk) where siteid = $2 and networkid = $1 
                                            )
                               AND typepk = (
                                                         SELECT typepk FROM fits.type WHERE typeid = $3
                                                       ) 
		AND methodpk = (
					SELECT methodpk FROM fits.method WHERE methodid = $4
				)
                                AND time > (now() - interval '`+strconv.Itoa(days)+` days')
                  		ORDER BY time ASC;`, networkID, siteID, typeID, methodID)
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}
	defer rows.Close()

	// Use a buffer for reading the data from the DB.  Then if a there
	// is an error we can let the client know without sending
	// a partial data response.
	var b bytes.Buffer
	b.Write([]byte("date-time, " + typeID + " (" + unit + "), error (" + unit + ")"))
	b.Write(eol)
	for rows.Next() {
		err := rows.Scan(&d)
		if err != nil {
			web.ServiceUnavailable(w, r, err)
			return
		}
		b.Write([]byte(d))
		b.Write(eol)
	}
	rows.Close()

	if methodID != "" {
		w.Header().Set("Content-Disposition", `attachment; filename="FITS-`+networkID+`-`+siteID+`-`+typeID+`-`+methodID+`.csv"`)
	} else {
		w.Header().Set("Content-Disposition", `attachment; filename="FITS-`+networkID+`-`+siteID+`-`+typeID+`.csv"`)
	}

	w.Header().Set("Content-Type", web.V1CSV)
	web.OkBuf(w, r, &b)
}
Example #7
0
func siteMap(w http.ResponseWriter, r *http.Request) {
	if err := siteMapD.CheckParams(r.URL.Query()); err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	v := r.URL.Query()

	bbox := v.Get("bbox")

	var insetBbox string

	if v.Get("insetBbox") != "" {
		insetBbox = v.Get("insetBbox")

		err := map180.ValidBbox(insetBbox)
		if err != nil {
			web.BadRequest(w, r, err.Error())
			return
		}
	}

	if v.Get("sites") == "" && (v.Get("siteID") == "" && v.Get("networkID") == "") {
		web.BadRequest(w, r, "please specify sites or networkID and siteID")
		return
	}

	if v.Get("sites") != "" && (v.Get("siteID") != "" || v.Get("networkID") != "") {
		web.BadRequest(w, r, "please specify either sites or networkID and siteID")
		return
	}

	if v.Get("sites") == "" && (v.Get("siteID") == "" || v.Get("networkID") == "") {
		web.BadRequest(w, r, "please specify networkID and siteID")
		return
	}

	err := map180.ValidBbox(bbox)
	if err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	width := 130

	if v.Get("width") != "" {
		width, err = strconv.Atoi(v.Get("width"))
		if err != nil {
			web.BadRequest(w, r, "invalid width.")
			return
		}
	}

	var s []st

	if v.Get("sites") != "" {
		for _, ns := range strings.Split(v.Get("sites"), ",") {
			nss := strings.Split(ns, ".")
			if len(nss) != 2 {
				web.BadRequest(w, r, "invalid sites query.")
				return
			}
			s = append(s, st{networkID: nss[0], siteID: nss[1]})
		}
	} else {
		s = append(s, st{networkID: v.Get("networkID"),
			siteID: v.Get("siteID")})
	}

	markers := make([]map180.Marker, 0)

	for _, site := range s {
		if !validSite(w, r, site.networkID, site.siteID) {
			return
		}

		g, err := geoJSONSite(site.networkID, site.siteID)
		if err != nil {
			web.ServiceUnavailable(w, r, err)
			return
		}

		m, err := geoJSONToMarkers(g)
		if err != nil {
			web.ServiceUnavailable(w, r, err)
			return
		}
		markers = append(markers, m...)

	}

	b, err := wm.SVG(bbox, width, markers, insetBbox)
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	w.Header().Set("Content-Type", "image/svg+xml")
	web.OkBuf(w, r, &b)
}
Example #8
0
func siteTypeMap(w http.ResponseWriter, r *http.Request) {
	if err := siteTypeMapD.CheckParams(r.URL.Query()); err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	v := r.URL.Query()

	bbox := v.Get("bbox")

	err := map180.ValidBbox(bbox)
	if err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	var insetBbox, typeID, methodID, within string
	width := 130

	if v.Get("insetBbox") != "" {
		insetBbox = v.Get("insetBbox")

		err := map180.ValidBbox(insetBbox)
		if err != nil {
			web.BadRequest(w, r, err.Error())
			return
		}
	}

	if v.Get("width") != "" {
		width, err = strconv.Atoi(v.Get("width"))
		if err != nil {
			web.BadRequest(w, r, "invalid width.")
			return
		}
	}
	if v.Get("methodID") != "" && v.Get("typeID") == "" {
		web.BadRequest(w, r, "typeID must be specified when methodID is specified.")
		return
	}

	if v.Get("typeID") != "" {
		typeID = v.Get("typeID")

		if !validType(w, r, typeID) {
			return
		}

		if v.Get("methodID") != "" {
			methodID = v.Get("methodID")
			if !validTypeMethod(w, r, typeID, methodID) {
				return
			}
		}
	}

	if v.Get("within") != "" {
		within = strings.Replace(v.Get("within"), "+", "", -1)
		if !validPoly(w, r, within) {
			return
		}
	} else if bbox != "" {
		within, err = map180.BboxToWKTPolygon(bbox)
		if err != nil {
			web.ServiceUnavailable(w, r, err)
			return
		}
	}

	g, err := geoJSONSites(typeID, methodID, within)
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	m, err := geoJSONToMarkers(g)
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	b, err := wm.SVG(bbox, width, m, insetBbox)
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	w.Header().Set("Content-Type", "image/svg+xml")
	web.OkBuf(w, r, &b)
}
Example #9
0
func spatialObs(w http.ResponseWriter, r *http.Request) {
	if err := spatialObsD.CheckParams(r.URL.Query()); err != nil {
		web.BadRequest(w, r, err.Error())
		return
	}

	v := r.URL.Query()

	var err error
	var days int
	days, err = strconv.Atoi(v.Get("days"))
	if err != nil || days > 7 || days <= 0 {
		web.BadRequest(w, r, "Invalid days query param.")
		return
	}

	start, err := time.Parse(time.RFC3339, v.Get("start"))
	if err != nil {
		web.BadRequest(w, r, "Invalid start query param.")
		return
	}

	end := start.Add(time.Duration(days) * time.Hour * 24)

	var srsName, authName string
	var srid int
	if v.Get("srsName") != "" {
		srsName = v.Get("srsName")
		srs := strings.Split(srsName, ":")
		if len(srs) != 2 {
			web.BadRequest(w, r, "Invalid srsName.")
			return
		}
		authName = srs[0]
		var err error
		srid, err = strconv.Atoi(srs[1])
		if err != nil {
			web.BadRequest(w, r, "Invalid srsName.")
			return
		}
		if !validSrs(w, r, authName, srid) {
			return
		}
	} else {
		srid = 4326
		authName = "EPSG"
		srsName = "EPSG:4326"
	}

	typeID := v.Get("typeID")

	var methodID string
	if v.Get("methodID") != "" {
		methodID = v.Get("methodID")
		if !validTypeMethod(w, r, typeID, methodID) {
			return
		}
	}

	var within string
	if v.Get("within") != "" {
		within = strings.Replace(v.Get("within"), "+", "", -1)
		if !validPoly(w, r, within) {
			return
		}
	}

	var unit string
	err = db.QueryRow("select symbol FROM fits.type join fits.unit using (unitPK) where typeID = $1", typeID).Scan(&unit)
	if err == sql.ErrNoRows {
		web.NotFound(w, r, "unit not found for typeID: "+typeID)
		return
	}
	if err != nil {
		web.ServiceUnavailable(w, r, err)
		return
	}

	var d string
	var rows *sql.Rows

	switch {
	case within == "" && methodID == "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s,%s,%s,%s,%s,%s,%s', networkid, siteid,  
		ST_X(ST_Transform(location::geometry, $4)), ST_Y(ST_Transform(location::geometry, $4)),
		height,ground_relationship, to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) 
		as csv FROM fits.observation join fits.site using (sitepk) join fits.network using (networkpk)
		WHERE typepk = (SELECT typepk FROM fits.type WHERE typeid = $1) AND 
		time >= $2 and time < $3 order by siteid asc`, typeID, start, end, srid)
	case within != "" && methodID == "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s,%s,%s,%s,%s,%s,%s', networkid, siteid,  
		ST_X(ST_Transform(location::geometry, $4)), ST_Y(ST_Transform(location::geometry, $4)),
		height,ground_relationship, to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) 
		as csv FROM fits.observation join fits.site using (sitepk) join fits.network using (networkpk)
		WHERE typepk = (SELECT typepk FROM fits.type WHERE typeid = $1) 
		AND  ST_Within(location::geometry, ST_GeomFromText($5, 4326))
		AND time >= $2 and time < $3 order by siteid asc`, typeID, start, end, srid, within)
	case within == "" && methodID != "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s,%s,%s,%s,%s,%s,%s', networkid, siteid,  
		ST_X(ST_Transform(location::geometry, $4)), ST_Y(ST_Transform(location::geometry, $4)),
		height,ground_relationship, to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) 
		as csv FROM fits.observation join fits.site using (sitepk) join fits.network using (networkpk)
		WHERE typepk = (SELECT typepk FROM fits.type WHERE typeid = $1) 
		AND methodpk = (SELECT methodpk FROM fits.method WHERE methodid = $5)
		AND time >= $2 and time < $3 order by siteid asc`, typeID, start, end, srid, methodID)
	case within != "" && methodID != "":
		rows, err = db.Query(
			`SELECT format('%s,%s,%s,%s,%s,%s,%s,%s,%s', networkid, siteid,  
		ST_X(ST_Transform(location::geometry, $4)), ST_Y(ST_Transform(location::geometry, $4)),
		height,ground_relationship, to_char(time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'), value, error) 
		as csv FROM fits.observation join fits.site using (sitepk) join fits.network using (networkpk)
		WHERE typepk = (SELECT typepk FROM fits.type WHERE typeid = $1) 
		AND methodpk = (SELECT methodpk FROM fits.method WHERE methodid = $6)
		AND  ST_Within(location::geometry, ST_GeomFromText($5, 4326))
		AND time >= $2 and time < $3 order by siteid asc`, typeID, start, end, srid, within, methodID)
	}
	if err != nil {
		// not sure what a transformation error would look like.
		// Return any errors as a 404.  Could improve this by inspecting
		// the error type to check for net dial errors that shoud 503.
		web.NotFound(w, r, err.Error())
		return
	}
	defer rows.Close()

	var b bytes.Buffer
	b.Write([]byte("networkID, siteID, X (" + srsName + "), Y (" + srsName + "), height, groundRelationship, date-time, " + typeID + " (" + unit + "), error (" + unit + ")"))
	b.Write(eol)
	for rows.Next() {
		err := rows.Scan(&d)
		if err != nil {
			web.ServiceUnavailable(w, r, err)
			return
		}
		b.Write([]byte(d))
		b.Write(eol)
	}
	rows.Close()

	w.Header().Set("Content-Type", web.V1CSV)
	if methodID != "" {
		w.Header().Set("Content-Disposition", `attachment; filename="FITS-`+typeID+`-`+methodID+`.csv"`)
	} else {
		w.Header().Set("Content-Disposition", `attachment; filename="FITS-`+typeID+`.csv"`)
	}

	web.OkBuf(w, r, &b)

}