DMRmap/backend/db.go
2026-02-19 17:00:59 +01:00

735 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"fmt"
"log"
"math"
"sort"
"strings"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
)
// StringArray maps a PostgreSQL TEXT[] column to a Go []string via database/sql.
type StringArray []string
func (a *StringArray) Scan(src interface{}) error {
if src == nil {
*a = StringArray{}
return nil
}
switch v := src.(type) {
case []byte:
parsed, err := parsePostgresArray(string(v))
if err != nil {
return err
}
*a = parsed
case string:
parsed, err := parsePostgresArray(v)
if err != nil {
return err
}
*a = parsed
default:
return fmt.Errorf("StringArray.Scan: unsupported type %T", src)
}
return nil
}
func (a StringArray) Value() (driver.Value, error) {
if len(a) == 0 {
return "{}", nil
}
elems := make([]string, len(a))
for i, s := range a {
escaped := strings.ReplaceAll(s, `\`, `\\`)
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
elems[i] = `"` + escaped + `"`
}
return "{" + strings.Join(elems, ",") + "}", nil
}
func parsePostgresArray(s string) ([]string, error) {
s = strings.TrimSpace(s)
if s == "{}" || s == "" {
return []string{}, nil
}
if s[0] != '{' || s[len(s)-1] != '}' {
return nil, fmt.Errorf("invalid postgres array literal: %q", s)
}
inner := s[1 : len(s)-1]
var result []string
var current strings.Builder
inQuote := false
escaped := false
for _, r := range inner {
if escaped {
current.WriteRune(r)
escaped = false
continue
}
if r == '\\' {
escaped = true
continue
}
if r == '"' {
inQuote = !inQuote
continue
}
if r == ',' && !inQuote {
result = append(result, current.String())
current.Reset()
continue
}
current.WriteRune(r)
}
result = append(result, current.String())
return result, nil
}
type Repeater struct {
ID int `json:"id"`
Callsign string `json:"callsign"`
FreqTx float64 `json:"freq_tx"`
FreqRx float64 `json:"freq_rx"`
FreqOffset string `json:"freq_offset"`
Band string `json:"band"`
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
City string `json:"city"`
State string `json:"state"`
Country string `json:"country"`
ColorCode int `json:"color_code"`
TsLinked string `json:"ts_linked"`
Trustee string `json:"trustee"`
IpscNetwork string `json:"ipsc_network"`
Networks StringArray `json:"networks"`
Hotspot int `json:"hotspot"`
Status string `json:"status"`
LastSeen *time.Time `json:"last_seen"`
BmStatus *int `json:"bm_status"`
BmStatusText string `json:"bm_status_text"`
Hardware string `json:"hardware"`
Firmware string `json:"firmware"`
Pep int `json:"pep"`
Agl int `json:"agl"`
Website string `json:"website"`
Description string `json:"description"`
ImportFreqInconsistent bool `json:"import_freq_inconsistent"`
Inactive bool `json:"inactive"`
LastPolled *time.Time `json:"last_polled"`
}
type Filter struct {
Field string `json:"field"`
Op string `json:"op"`
Value string `json:"value"`
}
func openDB(dsn string) (*sql.DB, error) {
var db *sql.DB
var err error
for i := 0; i < 30; i++ {
db, err = sql.Open("pgx", dsn)
if err != nil {
log.Printf("Failed to open database (attempt %d/30): %v", i+1, err)
time.Sleep(time.Second)
continue
}
if err = db.Ping(); err != nil {
db.Close()
log.Printf("Failed to ping database (attempt %d/30): %v", i+1, err)
time.Sleep(time.Second)
continue
}
break
}
if err != nil {
return nil, fmt.Errorf("failed to connect to database after 30 attempts: %w", err)
}
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
return db, nil
}
func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band string, networks []string, showHotspots bool, showInactive bool) ([]Repeater, error) {
paramIdx := 0
nextParam := func() string {
paramIdx++
return fmt.Sprintf("$%d", paramIdx)
}
query := `SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
import_freq_inconsistent, last_polled
FROM repeaters WHERE lat BETWEEN ` + nextParam() + ` AND ` + nextParam() + ` AND lng BETWEEN ` + nextParam() + ` AND ` + nextParam()
args := []interface{}{minLat, maxLat, minLng, maxLng}
switch band {
case "2m":
query += " AND band = " + nextParam()
args = append(args, "2m")
case "70cm":
query += " AND band = " + nextParam()
args = append(args, "70cm")
default:
query += " AND band IN ('2m', '70cm')"
}
if !showHotspots {
query += " AND hotspot = 0"
}
// Network filter: only apply when not all 4 categories are selected
if len(networks) > 0 && len(networks) < 4 {
var networkValues []string
hasOther := false
for _, n := range networks {
switch n {
case "BM":
networkValues = append(networkValues, "Brandmeister")
case "DMR+":
networkValues = append(networkValues, "DMR+")
case "TGIF":
networkValues = append(networkValues, "TGIF")
case "Other":
hasOther = true
networkValues = append(networkValues, "DMR-MARC", "FreeDMR", "Other")
}
}
if len(networkValues) > 0 {
p := nextParam()
if hasOther {
query += " AND (networks && " + p + "::text[] OR networks = '{}')"
} else {
query += " AND networks && " + p + "::text[]"
}
args = append(args, StringArray(networkValues))
}
}
if !showInactive {
threshold := time.Now().Add(-7 * 24 * time.Hour)
query += " AND (last_seen IS NULL OR last_seen >= " + nextParam() + ") AND (bm_status IS NULL OR bm_status != 0)"
args = append(args, threshold)
}
rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
threshold := time.Now().Add(-7 * 24 * time.Hour)
var results []Repeater
for rows.Next() {
var r Repeater
if err := rows.Scan(&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
&r.ColorCode, &r.TsLinked, &r.Trustee,
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
&r.ImportFreqInconsistent, &r.LastPolled); err != nil {
return nil, err
}
r.Inactive = (r.LastSeen != nil && r.LastSeen.Before(threshold)) || (r.BmStatus != nil && *r.BmStatus == 0)
results = append(results, r)
}
if err := rows.Err(); err != nil {
return nil, err
}
return results, nil
}
// Route corridor query: find repeaters within corridorKm of a polyline.
func queryRepeatersAlongRoute(db *sql.DB, points [][2]float64, corridorKm float64, band string, networks []string, showHotspots bool, showInactive bool) ([]RepeaterWithDistance, error) {
if len(points) == 0 {
return []RepeaterWithDistance{}, nil
}
// Compute bounding box of all route points + corridor padding
minLat, maxLat := points[0][0], points[0][0]
minLng, maxLng := points[0][1], points[0][1]
for _, p := range points {
if p[0] < minLat {
minLat = p[0]
}
if p[0] > maxLat {
maxLat = p[0]
}
if p[1] < minLng {
minLng = p[1]
}
if p[1] > maxLng {
maxLng = p[1]
}
}
latPad := corridorKm / 111.32
avgLat := (minLat + maxLat) / 2
lngPad := corridorKm / (111.32 * math.Cos(avgLat*math.Pi/180))
minLat -= latPad
maxLat += latPad
minLng -= lngPad
maxLng += lngPad
// Fetch candidates from bounding box
candidates, err := queryRepeaters(db, minLat, maxLat, minLng, maxLng, band, networks, showHotspots, showInactive)
if err != nil {
return nil, err
}
// Pre-compute cumulative route distances from start
cumDist := make([]float64, len(points))
for i := 1; i < len(points); i++ {
cumDist[i] = cumDist[i-1] + haversineKm(points[i-1][0], points[i-1][1], points[i][0], points[i][1])
}
// Filter by perpendicular distance to route; record along-route distance from start
var results []RepeaterWithDistance
for _, r := range candidates {
perpDist, alongDist := routeDistances(r.Lat, r.Lng, points, cumDist)
if perpDist <= corridorKm {
results = append(results, RepeaterWithDistance{Repeater: r, Distance: math.Round(alongDist*10) / 10})
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].Distance < results[j].Distance
})
return results, nil
}
// Radius query: find repeaters within radiusKm of a point, sorted by distance.
type RepeaterWithDistance struct {
Repeater
Distance float64 `json:"distance"`
}
func queryRepeatersInRadius(db *sql.DB, lat, lng, radiusKm float64, band string, networks []string, showHotspots bool, showInactive bool) ([]RepeaterWithDistance, error) {
latPad := radiusKm / 111.32
lngPad := radiusKm / (111.32 * math.Cos(lat*math.Pi/180))
candidates, err := queryRepeaters(db, lat-latPad, lat+latPad, lng-lngPad, lng+lngPad, band, networks, showHotspots, showInactive)
if err != nil {
return nil, err
}
var results []RepeaterWithDistance
for _, r := range candidates {
d := haversineKm(lat, lng, r.Lat, r.Lng)
if d <= radiusKm {
results = append(results, RepeaterWithDistance{Repeater: r, Distance: math.Round(d*10) / 10})
}
}
sort.Slice(results, func(i, j int) bool {
return results[i].Distance < results[j].Distance
})
return results, nil
}
// Admin query: paginated list of all repeaters with optional search.
func queryAdminRepeaters(db *sql.DB, search string, page, perPage int, filters []Filter, filterMode string) ([]Repeater, int, error) {
paramIdx := 0
nextParam := func() string {
paramIdx++
return fmt.Sprintf("$%d", paramIdx)
}
where := ""
var args []interface{}
if search != "" {
p := nextParam()
where = " WHERE (callsign ILIKE " + p +
" OR city ILIKE " + p +
" OR state ILIKE " + p +
" OR country ILIKE " + p +
" OR array_to_string(networks, ',') ILIKE " + p +
" OR id::text = " + nextParam() + ")"
pattern := "%" + search + "%"
args = append(args, pattern, search)
}
if len(filters) > 0 {
var clauses []string
for _, f := range filters {
switch f.Field {
case "country":
switch f.Op {
case "eq":
clauses = append(clauses, "country = "+nextParam())
args = append(args, f.Value)
case "neq":
clauses = append(clauses, "country != "+nextParam())
args = append(args, f.Value)
case "contains":
clauses = append(clauses, "country ILIKE "+nextParam())
args = append(args, "%"+f.Value+"%")
}
case "bm_status":
switch f.Op {
case "empty":
clauses = append(clauses, "bm_status IS NULL")
case "not_empty":
clauses = append(clauses, "bm_status IS NOT NULL")
}
}
}
if len(clauses) > 0 {
joiner := " AND "
if filterMode == "or" {
joiner = " OR "
}
filterExpr := "(" + strings.Join(clauses, joiner) + ")"
if where == "" {
where = " WHERE " + filterExpr
} else {
where += " AND " + filterExpr
}
}
}
var total int
err := db.QueryRow("SELECT COUNT(*) FROM repeaters"+where, args...).Scan(&total)
if err != nil {
return nil, 0, err
}
offset := (page - 1) * perPage
query := `SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
import_freq_inconsistent, last_polled
FROM repeaters` + where + ` ORDER BY callsign LIMIT ` + nextParam() + ` OFFSET ` + nextParam()
args = append(args, perPage, offset)
rows, err := db.Query(query, args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
threshold := time.Now().Add(-7 * 24 * time.Hour)
var results []Repeater
for rows.Next() {
var r Repeater
if err := rows.Scan(&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
&r.ColorCode, &r.TsLinked, &r.Trustee,
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
&r.ImportFreqInconsistent, &r.LastPolled); err != nil {
return nil, 0, err
}
r.Inactive = (r.LastSeen != nil && r.LastSeen.Before(threshold)) || (r.BmStatus != nil && *r.BmStatus == 0)
results = append(results, r)
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
if results == nil {
results = []Repeater{}
}
return results, total, nil
}
// routeDistances returns the perpendicular distance from a point to the nearest
// route segment, and the along-route distance from the route start to the
// projection point on that segment.
func routeDistances(lat, lng float64, points [][2]float64, cumDist []float64) (perpDist, alongDist float64) {
bestPerp := math.Inf(1)
bestAlong := 0.0
for i := 0; i < len(points)-1; i++ {
d, t := distToSegmentWithParam(lat, lng, points[i][0], points[i][1], points[i+1][0], points[i+1][1])
if d < bestPerp {
bestPerp = d
segLen := cumDist[i+1] - cumDist[i]
bestAlong = cumDist[i] + t*segLen
}
}
if len(points) == 1 {
bestPerp = haversineKm(lat, lng, points[0][0], points[0][1])
}
return bestPerp, bestAlong
}
// distToSegmentWithParam returns the approximate distance from point P to line
// segment AB in km, and the projection parameter t (01) along the segment.
func distToSegmentWithParam(pLat, pLng, aLat, aLng, bLat, bLng float64) (dist, t float64) {
cosLat := math.Cos(pLat * math.Pi / 180)
// Project to approximate planar coordinates (km)
px := (pLng - aLng) * cosLat * 111.32
py := (pLat - aLat) * 111.32
bx := (bLng - aLng) * cosLat * 111.32
by := (bLat - aLat) * 111.32
lenSq := bx*bx + by*by
if lenSq == 0 {
return math.Sqrt(px*px + py*py), 0
}
t = (px*bx + py*by) / lenSq
if t < 0 {
t = 0
}
if t > 1 {
t = 1
}
dx := px - t*bx
dy := py - t*by
return math.Sqrt(dx*dx + dy*dy), t
}
func queryRepeaterSearch(db *sql.DB, search string, limit int) ([]Repeater, int, error) {
if limit <= 0 || limit > 200 {
limit = 25
}
pattern := "%" + search + "%"
where := `WHERE callsign ILIKE $1 OR city ILIKE $1 OR state ILIKE $1 OR country ILIKE $1 OR array_to_string(networks, ',') ILIKE $1 OR id::text = $2`
var total int
err := db.QueryRow("SELECT COUNT(*) FROM repeaters "+where, pattern, search).Scan(&total)
if err != nil {
return nil, 0, err
}
rows, err := db.Query(`SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
import_freq_inconsistent, last_polled
FROM repeaters `+where+` ORDER BY callsign LIMIT $3`, pattern, search, limit)
if err != nil {
return nil, 0, err
}
defer rows.Close()
threshold := time.Now().Add(-7 * 24 * time.Hour)
var results []Repeater
for rows.Next() {
var r Repeater
if err := rows.Scan(&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
&r.ColorCode, &r.TsLinked, &r.Trustee,
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
&r.ImportFreqInconsistent, &r.LastPolled); err != nil {
return nil, 0, err
}
r.Inactive = (r.LastSeen != nil && r.LastSeen.Before(threshold)) || (r.BmStatus != nil && *r.BmStatus == 0)
results = append(results, r)
}
if results == nil {
results = []Repeater{}
}
return results, total, rows.Err()
}
func queryRepeaterByID(db *sql.DB, id int) (*Repeater, error) {
var r Repeater
err := db.QueryRow(`SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
import_freq_inconsistent, last_polled
FROM repeaters WHERE id = $1`, id).Scan(
&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
&r.ColorCode, &r.TsLinked, &r.Trustee,
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
&r.ImportFreqInconsistent, &r.LastPolled)
if err != nil {
return nil, err
}
threshold := time.Now().Add(-7 * 24 * time.Hour)
r.Inactive = (r.LastSeen != nil && r.LastSeen.Before(threshold)) || (r.BmStatus != nil && *r.BmStatus == 0)
return &r, nil
}
func updateRepeater(db *sql.DB, r Repeater) error {
_, err := db.Exec(`UPDATE repeaters SET
callsign=$2, freq_tx=$3, freq_rx=$4, freq_offset=$5, band=$6,
lat=$7, lng=$8, city=$9, state=$10, country=$11,
color_code=$12, ts_linked=$13, trustee=$14, ipsc_network=$15,
networks=$16, hotspot=$17, status=$18,
hardware=$19, firmware=$20, pep=$21, agl=$22,
website=$23, description=$24,
last_seen=$25, bm_status=$26, bm_status_text=$27
WHERE id=$1`,
r.ID, r.Callsign, r.FreqTx, r.FreqRx, r.FreqOffset, r.Band,
r.Lat, r.Lng, r.City, r.State, r.Country,
r.ColorCode, r.TsLinked, r.Trustee, r.IpscNetwork,
r.Networks, r.Hotspot, r.Status,
r.Hardware, r.Firmware, r.Pep, r.Agl,
r.Website, r.Description,
r.LastSeen, r.BmStatus, r.BmStatusText)
return err
}
type ChangelogEntry struct {
RepeaterID int
Callsign string
Source string
Action string
Description string
OldValues map[string]interface{}
NewValues map[string]interface{}
}
func insertChangelog(db *sql.DB, entry ChangelogEntry) {
var oldJSON, newJSON interface{}
if entry.OldValues != nil {
if b, err := json.Marshal(entry.OldValues); err == nil {
oldJSON = string(b)
}
}
if entry.NewValues != nil {
if b, err := json.Marshal(entry.NewValues); err == nil {
newJSON = string(b)
}
}
_, err := db.Exec(
`INSERT INTO changelog (repeater_id, callsign, source, action, description, old_values, new_values)
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
entry.RepeaterID, entry.Callsign, entry.Source, entry.Action, entry.Description,
oldJSON, newJSON,
)
if err != nil {
log.Printf("changelog: failed to insert entry for repeater %d: %v", entry.RepeaterID, err)
}
}
func diffRepeater(old, new Repeater) (oldVals, newVals map[string]interface{}) {
oldVals = make(map[string]interface{})
newVals = make(map[string]interface{})
if old.Callsign != new.Callsign {
oldVals["callsign"] = old.Callsign
newVals["callsign"] = new.Callsign
}
if old.FreqTx != new.FreqTx {
oldVals["freq_tx"] = old.FreqTx
newVals["freq_tx"] = new.FreqTx
}
if old.FreqRx != new.FreqRx {
oldVals["freq_rx"] = old.FreqRx
newVals["freq_rx"] = new.FreqRx
}
if old.FreqOffset != new.FreqOffset {
oldVals["freq_offset"] = old.FreqOffset
newVals["freq_offset"] = new.FreqOffset
}
if old.Band != new.Band {
oldVals["band"] = old.Band
newVals["band"] = new.Band
}
if old.Lat != new.Lat {
oldVals["lat"] = old.Lat
newVals["lat"] = new.Lat
}
if old.Lng != new.Lng {
oldVals["lng"] = old.Lng
newVals["lng"] = new.Lng
}
if old.City != new.City {
oldVals["city"] = old.City
newVals["city"] = new.City
}
if old.State != new.State {
oldVals["state"] = old.State
newVals["state"] = new.State
}
if old.Country != new.Country {
oldVals["country"] = old.Country
newVals["country"] = new.Country
}
if old.ColorCode != new.ColorCode {
oldVals["color_code"] = old.ColorCode
newVals["color_code"] = new.ColorCode
}
if old.TsLinked != new.TsLinked {
oldVals["ts_linked"] = old.TsLinked
newVals["ts_linked"] = new.TsLinked
}
if old.Trustee != new.Trustee {
oldVals["trustee"] = old.Trustee
newVals["trustee"] = new.Trustee
}
if old.IpscNetwork != new.IpscNetwork {
oldVals["ipsc_network"] = old.IpscNetwork
newVals["ipsc_network"] = new.IpscNetwork
}
if strings.Join([]string(old.Networks), ",") != strings.Join([]string(new.Networks), ",") {
oldVals["networks"] = old.Networks
newVals["networks"] = new.Networks
}
if old.Hotspot != new.Hotspot {
oldVals["hotspot"] = old.Hotspot
newVals["hotspot"] = new.Hotspot
}
if old.Status != new.Status {
oldVals["status"] = old.Status
newVals["status"] = new.Status
}
if old.Hardware != new.Hardware {
oldVals["hardware"] = old.Hardware
newVals["hardware"] = new.Hardware
}
if old.Firmware != new.Firmware {
oldVals["firmware"] = old.Firmware
newVals["firmware"] = new.Firmware
}
if old.Pep != new.Pep {
oldVals["pep"] = old.Pep
newVals["pep"] = new.Pep
}
if old.Agl != new.Agl {
oldVals["agl"] = old.Agl
newVals["agl"] = new.Agl
}
if old.Website != new.Website {
oldVals["website"] = old.Website
newVals["website"] = new.Website
}
if old.Description != new.Description {
oldVals["description"] = old.Description
newVals["description"] = new.Description
}
if old.BmStatusText != new.BmStatusText {
oldVals["bm_status_text"] = old.BmStatusText
newVals["bm_status_text"] = new.BmStatusText
}
if len(oldVals) == 0 {
return nil, nil
}
return oldVals, newVals
}
func haversineKm(lat1, lng1, lat2, lng2 float64) float64 {
const r = 6371.0
dLat := (lat2 - lat1) * math.Pi / 180
dLng := (lng2 - lng1) * math.Pi / 180
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
math.Cos(lat1*math.Pi/180)*math.Cos(lat2*math.Pi/180)*
math.Sin(dLng/2)*math.Sin(dLng/2)
return r * 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
}