First prototype
This commit is contained in:
parent
52e6e094d5
commit
fba8cc058e
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
||||||
*.dll
|
*.dll
|
||||||
*.so
|
*.so
|
||||||
*.dylib
|
*.dylib
|
||||||
|
/ot-browser
|
||||||
|
|
||||||
# Test binary, built with `go test -c`
|
# Test binary, built with `go test -c`
|
||||||
*.test
|
*.test
|
||||||
|
@ -15,3 +16,5 @@
|
||||||
# Dependency directories (remove the comment below to include it)
|
# Dependency directories (remove the comment below to include it)
|
||||||
# vendor/
|
# vendor/
|
||||||
|
|
||||||
|
# Config files
|
||||||
|
*.ini
|
||||||
|
|
11
Dockerfile
Normal file
11
Dockerfile
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
FROM golang:alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
ADD . /app
|
||||||
|
RUN apk update && apk add build-base
|
||||||
|
RUN cd /app && make requirements && make ot-browser-static
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=builder /app/www /www
|
||||||
|
COPY --from=builder /app/ot-recorder /usr/bin/ot-recorder
|
||||||
|
ENTRYPOINT ["/usr/bin/ot-browser", "/www"]
|
21
Makefile
Normal file
21
Makefile
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
default: all
|
||||||
|
all: ot-browser
|
||||||
|
|
||||||
|
requirements:
|
||||||
|
go get github.com/eclipse/paho.mqtt.golang
|
||||||
|
|
||||||
|
ot-browser: cmd/ot-browser/*.go
|
||||||
|
go build -o ot-browser $^
|
||||||
|
|
||||||
|
ot-browser-static: cmd/ot-browser/*.go
|
||||||
|
export CGO_ENABLED=0
|
||||||
|
CGO_ENABLED=0 go build -ldflags="-w -s" -o ot-browser $^
|
||||||
|
|
||||||
|
|
||||||
|
container: Dockerfile cmd/ot-browser/*.go
|
||||||
|
docker build . -t feldspaten.org/ot-browser
|
||||||
|
|
||||||
|
#deploy: Dockerfile cmd/ot-browser/*.go
|
||||||
|
# docker build . -t grisu48/ot-browser
|
||||||
|
# docker push grisu48/ot-browser
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("ot-browser")
|
|
||||||
}
|
|
22
cmd/ot-browser/geojson.go
Normal file
22
cmd/ot-browser/geojson.go
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
type GeoJSON struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Geometry Geometry `json:"geometry"`
|
||||||
|
Properties map[string]string `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Geometry struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Coordinates [2]float32 `json:"coordinates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreatePoint(lon float32, lat float32) GeoJSON {
|
||||||
|
var geojson GeoJSON
|
||||||
|
geojson.Type = "Feature"
|
||||||
|
geojson.Properties = make(map[string]string, 0)
|
||||||
|
geojson.Geometry.Type = "Point"
|
||||||
|
geojson.Geometry.Coordinates[0] = lon
|
||||||
|
geojson.Geometry.Coordinates[1] = lat
|
||||||
|
return geojson
|
||||||
|
}
|
58
cmd/ot-browser/location.go
Normal file
58
cmd/ot-browser/location.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Timestamp int64
|
||||||
|
Lon float32
|
||||||
|
Lat float32
|
||||||
|
Alt float32
|
||||||
|
Acc float32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance computes the distance (in meters) between two points
|
||||||
|
func (loc *Location) Distance(loc2 Location) float32 {
|
||||||
|
R := 6371e3 // metres
|
||||||
|
// Angles are in radians ( * math.Pi/180.0)
|
||||||
|
phi1 := loc.Lat * math.Pi / 180.0
|
||||||
|
phi2 := loc.Lon * math.Pi / 180.0
|
||||||
|
deltaPhi := (loc2.Lat - loc.Lat) * math.Pi / 180.0
|
||||||
|
deltaLambda := (loc2.Lon - loc.Lon) * math.Pi / 180.0
|
||||||
|
|
||||||
|
a := math.Sin(float64(deltaPhi/2))*math.Sin(float64(deltaPhi/2.0)) +
|
||||||
|
math.Cos(float64(phi1))*math.Cos(float64(phi2))*
|
||||||
|
math.Sin(float64(deltaLambda/2))*math.Sin(float64(deltaLambda/2))
|
||||||
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||||
|
return float32(R * c)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Basic plausability check for the values */
|
||||||
|
func (l *Location) IsPlausible() bool {
|
||||||
|
// I keep those two comparisons for now
|
||||||
|
if l.Timestamp == 0 && l.Lon == 0 && l.Lat == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Sometimes I get updates with no location but a valid timestamp. Don't record those
|
||||||
|
if l.Lon == 0 && l.Lat == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Out of bounds
|
||||||
|
if l.Lon < -180 || l.Lon > 180 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if l.Lat < -90 || l.Lat > 90 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Negative accuracy is not accepted
|
||||||
|
if l.Acc < 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l Location) String() string {
|
||||||
|
return fmt.Sprintf("Location{%d,%5.2f,%5.2f,%5.2f,%3.0f}", l.Timestamp, l.Lon, l.Lat, l.Alt, l.Acc)
|
||||||
|
}
|
182
cmd/ot-browser/ot-browser.go
Normal file
182
cmd/ot-browser/ot-browser.go
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/ini.v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
wwwDir string
|
||||||
|
bindAddr string
|
||||||
|
mqttRemote string
|
||||||
|
}
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
var mqtt MQTTReceiver
|
||||||
|
var devices map[string]Location
|
||||||
|
|
||||||
|
func (c *Config) SetDefaults() {
|
||||||
|
c.wwwDir = ""
|
||||||
|
c.bindAddr = "127.0.0.1:8090"
|
||||||
|
c.mqttRemote = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func mqtt_recv(id string, loc Location) {
|
||||||
|
devices[id] = loc
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseProgramArguments() error {
|
||||||
|
args := os.Args[1:]
|
||||||
|
for i := 0; i < len(args); i++ {
|
||||||
|
arg := args[i]
|
||||||
|
if len(arg) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if arg == "-h" || arg == "--help" {
|
||||||
|
fmt.Printf("Usage: %s [OPTIONS]", os.Args[0])
|
||||||
|
fmt.Println(" -h,--help Print this help message")
|
||||||
|
fmt.Println(" -w,--www DIR Set WWW content directory")
|
||||||
|
fmt.Println(" -c,--config FILE Read configuration from the ini FILE")
|
||||||
|
fmt.Println(" -b,--bind ADDR Bind webserver to ADDR")
|
||||||
|
os.Exit(0)
|
||||||
|
} else if arg == "-w" || arg == "--www" {
|
||||||
|
i++
|
||||||
|
config.wwwDir = args[i]
|
||||||
|
} else if arg == "-c" || arg == "--config" {
|
||||||
|
i++
|
||||||
|
if err := config.ReadFile(args[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if arg == "-b" || arg == "--bind" {
|
||||||
|
i++
|
||||||
|
config.bindAddr = args[i]
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("Invalid argument: %s\n", arg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("ot-browser")
|
||||||
|
devices = make(map[string]Location, 0)
|
||||||
|
config.SetDefaults()
|
||||||
|
parseProgramArguments()
|
||||||
|
|
||||||
|
// Connect mqtt
|
||||||
|
if config.mqttRemote == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "No mqtt remote set\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
mqtt.Received = mqtt_recv
|
||||||
|
if err := mqtt.Connect(config.mqttRemote, "owntracks/#", "", "", "ot-browser"); err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "mqtt error: %s\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup webserver
|
||||||
|
fs := http.FileServer(http.Dir(config.wwwDir))
|
||||||
|
http.Handle("/", http.StripPrefix("/", fs))
|
||||||
|
http.HandleFunc("/devices", handlerDevices)
|
||||||
|
http.HandleFunc("/devices.json", handlerDevices)
|
||||||
|
http.HandleFunc("/locations", handlerLocations)
|
||||||
|
http.HandleFunc("/devices/", handlerDeviceQuery)
|
||||||
|
fmt.Println("Serving: http://" + config.bindAddr)
|
||||||
|
log.Fatal(http.ListenAndServe(config.bindAddr, nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ReadFile(filename string) error {
|
||||||
|
cfg, err := ini.InsensitiveLoad(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mqtt := cfg.Section("mqtt")
|
||||||
|
if val := mqtt.Key("remote").String(); val != "" {
|
||||||
|
c.mqttRemote = val
|
||||||
|
}
|
||||||
|
www := cfg.Section("www")
|
||||||
|
if val := www.Key("remote").String(); val != "" {
|
||||||
|
c.wwwDir = val
|
||||||
|
}
|
||||||
|
if val := mqtt.Key("bind").String(); val != "" {
|
||||||
|
c.bindAddr = val
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerDevices(w http.ResponseWriter, r *http.Request) {
|
||||||
|
devs := make([]string, 0)
|
||||||
|
for dev := range devices {
|
||||||
|
devs = append(devs, dev)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := json.Marshal(devs)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("Server error"))
|
||||||
|
fmt.Fprintf(os.Stderr, "json marshal error: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerLocations(w http.ResponseWriter, r *http.Request) {
|
||||||
|
locations := make([]GeoJSON, 0)
|
||||||
|
for dev, loc := range devices {
|
||||||
|
point := CreatePoint(loc.Lon, loc.Lat)
|
||||||
|
point.Properties["name"] = dev
|
||||||
|
point.Properties["id"] = dev
|
||||||
|
point.Properties["time"] = fmt.Sprintf("%d", loc.Timestamp)
|
||||||
|
point.Properties["altitude"] = fmt.Sprintf("%d", loc.Alt)
|
||||||
|
point.Properties["accuracy"] = fmt.Sprintf("%d", loc.Acc)
|
||||||
|
locations = append(locations, point)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, err := json.Marshal(locations)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("Server error"))
|
||||||
|
fmt.Fprintf(os.Stderr, "json marshal error: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write([]byte("{ \"features\": "))
|
||||||
|
w.Write(buf)
|
||||||
|
w.Write([]byte("}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func handlerDeviceQuery(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// extract device name from the path
|
||||||
|
path := r.URL.Path
|
||||||
|
device := ""
|
||||||
|
if i := strings.Index(path, "devices/"); i > 0 {
|
||||||
|
device = path[i+8:]
|
||||||
|
} else {
|
||||||
|
goto server_Error
|
||||||
|
}
|
||||||
|
|
||||||
|
if loc, ok := devices[device]; ok {
|
||||||
|
buf, err := json.Marshal(loc)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("Server error"))
|
||||||
|
fmt.Fprintf(os.Stderr, "json marshal error: %s\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Write(buf)
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
w.Write([]byte("device not found"))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
server_Error:
|
||||||
|
w.WriteHeader(500)
|
||||||
|
w.Write([]byte("Server error"))
|
||||||
|
return
|
||||||
|
}
|
159
cmd/ot-browser/receiver.go
Normal file
159
cmd/ot-browser/receiver.go
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
MQTT "github.com/eclipse/paho.mqtt.golang"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MQTTCallback func(id string, loc Location)
|
||||||
|
|
||||||
|
type MQTTReceiver struct {
|
||||||
|
mqtt MQTT.Client
|
||||||
|
remote string
|
||||||
|
topic string
|
||||||
|
connected bool
|
||||||
|
Received MQTTCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mqtt *MQTTReceiver) Connect(remote string, topic string, username string, password string, clientid string) error {
|
||||||
|
// Ensure topics ends with '#'
|
||||||
|
if !strings.HasSuffix(topic, "#") {
|
||||||
|
if !strings.HasSuffix(topic, "/") {
|
||||||
|
topic += "/#"
|
||||||
|
} else {
|
||||||
|
topic += "#"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add default port to remote if not existing
|
||||||
|
// TODO: IPv6 handling is not yet implemented
|
||||||
|
if !strings.Contains(remote, ":") {
|
||||||
|
remote += ":1883"
|
||||||
|
}
|
||||||
|
|
||||||
|
mqtt.remote = remote
|
||||||
|
mqtt.topic = topic
|
||||||
|
opts := MQTT.NewClientOptions().AddBroker("tcp://" + remote)
|
||||||
|
if username != "" {
|
||||||
|
opts.SetUsername(username)
|
||||||
|
}
|
||||||
|
if password != "" {
|
||||||
|
opts.SetPassword(password)
|
||||||
|
}
|
||||||
|
if clientid != "" {
|
||||||
|
opts.SetClientID(clientid)
|
||||||
|
}
|
||||||
|
opts.SetKeepAlive(20 * time.Second)
|
||||||
|
opts.SetConnectionLostHandler(mqtt.mqttConnectionLost)
|
||||||
|
opts.SetOnConnectHandler(mqtt.mqttConnected)
|
||||||
|
opts.SetAutoReconnect(true)
|
||||||
|
c := MQTT.NewClient(opts)
|
||||||
|
mqtt.mqtt = c
|
||||||
|
// TODO: Add listener also if initial connection fails (and attempt reconnects)
|
||||||
|
if token := c.Connect(); token.Wait() && token.Error() != nil {
|
||||||
|
log.Println(fmt.Sprintf("Error connecting to MQTT %s - %s", remote, token.Error()))
|
||||||
|
} else {
|
||||||
|
if token := c.Subscribe(topic, 0, mqtt.mqttReceive); token.Wait() && token.Error() != nil {
|
||||||
|
log.Println(fmt.Sprintf("Error subscribing listener %s - %s", remote, token.Error()))
|
||||||
|
} else {
|
||||||
|
log.Printf("MQTT %s subscribed to topic '%s'", remote, topic)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mqtt *MQTTReceiver) mqttReceive(client MQTT.Client, msg MQTT.Message) {
|
||||||
|
payload := string(msg.Payload())
|
||||||
|
topic := msg.Topic()
|
||||||
|
if strings.HasPrefix(topic, "owntracks/") {
|
||||||
|
// Topic: owntracks/USER/DEVICE
|
||||||
|
// Split topic to user and device
|
||||||
|
topic = topic[10:]
|
||||||
|
i := strings.Index(topic, "/")
|
||||||
|
if i <= 0 {
|
||||||
|
log.Printf("Illegal owntracks topic received\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username := topic[:i]
|
||||||
|
devicename := topic[i+1:]
|
||||||
|
|
||||||
|
i = strings.Index(devicename, "/")
|
||||||
|
if i > 0 { // Ignore special device topics (e.g. 'cmd' or 'events')
|
||||||
|
//fmt.Printf("Ignoring topic %s \n", topic)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
identifier := fmt.Sprintf("%s/%s", username, devicename)
|
||||||
|
loc, err := parseLocationJson(payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error processing JSON (MQTT) : %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if mqtt.Received != nil {
|
||||||
|
mqtt.Received(identifier, loc)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Invalid topic. Ignore for now.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mqtt *MQTTReceiver) mqttConnectionLost(client MQTT.Client, err error) {
|
||||||
|
mqtt.connected = false
|
||||||
|
options := client.OptionsReader()
|
||||||
|
remotes := options.Servers()
|
||||||
|
remote := ""
|
||||||
|
if len(remotes) > 0 {
|
||||||
|
// TODO:: What to do if there are more?
|
||||||
|
remote = remotes[0].String()
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
fmt.Fprintf(os.Stderr, "Reconnecting to %s after failure: %s\n", remote, err)
|
||||||
|
token := client.Connect()
|
||||||
|
if token.Wait() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("MQTT connection %s established", remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mqtt *MQTTReceiver) mqttConnected(client MQTT.Client) {
|
||||||
|
log.Printf("MQTT connected: %s", mqtt.remote)
|
||||||
|
mqtt.connected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse json as location */
|
||||||
|
func parseLocationJson(buf string) (Location, error) {
|
||||||
|
var dat map[string]interface{}
|
||||||
|
var loc Location
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(buf), &dat); err != nil {
|
||||||
|
return loc, err
|
||||||
|
}
|
||||||
|
// Get contents from json message
|
||||||
|
/* FORMER check to ensure we get "location" types
|
||||||
|
if dat["_type"].(string) != "location" {
|
||||||
|
return loc, errors.New("Illegal json type")
|
||||||
|
} else {
|
||||||
|
*/
|
||||||
|
if dat["lon"] != nil {
|
||||||
|
loc.Lon = float32(dat["lon"].(float64))
|
||||||
|
}
|
||||||
|
if dat["lat"] != nil {
|
||||||
|
loc.Lat = float32(dat["lat"].(float64))
|
||||||
|
}
|
||||||
|
if dat["alt"] != nil {
|
||||||
|
loc.Alt = float32(dat["alt"].(float64))
|
||||||
|
}
|
||||||
|
if dat["acc"] != nil {
|
||||||
|
loc.Acc = float32(dat["acc"].(float64))
|
||||||
|
}
|
||||||
|
if dat["tst"] != nil {
|
||||||
|
loc.Timestamp = int64(dat["tst"].(float64))
|
||||||
|
}
|
||||||
|
return loc, nil
|
||||||
|
}
|
6
go.mod
6
go.mod
|
@ -1,3 +1,9 @@
|
||||||
module feldspaten.org/ot-browser/v2
|
module feldspaten.org/ot-browser/v2
|
||||||
|
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.3.4
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.62.0
|
||||||
|
)
|
||||||
|
|
22
go.sum
Normal file
22
go.sum
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.3.4 h1:/sS2PA+PgomTO1bfJSDJncox+U7X5Boa3AfhEywYdgI=
|
||||||
|
github.com/eclipse/paho.mqtt.golang v1.3.4/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||||
|
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||||
|
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||||
|
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||||
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||||
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||||
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
|
||||||
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
gopkg.in/ini.v1 v1.62.0 h1:duBzk771uxoUuOlyRLkHsygud9+5lrlGjdFBb4mSKDU=
|
||||||
|
gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
8
ot-browser.ini.example
Normal file
8
ot-browser.ini.example
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
# Configuration file for ot-browser
|
||||||
|
|
||||||
|
[mqtt]
|
||||||
|
remote = 127.0.0.1
|
||||||
|
|
||||||
|
[www]
|
||||||
|
dir = ./www
|
||||||
|
bind = 127.0.0.1:8090
|
BIN
www/images/marker-icon.png
Normal file
BIN
www/images/marker-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.7 KiB |
BIN
www/images/marker-shadow.png
Normal file
BIN
www/images/marker-shadow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 797 B |
15
www/index.html
Normal file
15
www/index.html
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>ot-browser</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<link rel="stylesheet" href="leaflet.css" />
|
||||||
|
<link rel="stylesheet" href="site.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="map"></div>
|
||||||
|
<script src="leaflet-src.js"></script>
|
||||||
|
<script src="leaflet-realtime.js"></script>
|
||||||
|
<script src="index.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
40
www/index.js
Normal file
40
www/index.js
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
var map = L.map('map'),
|
||||||
|
realtime = L.realtime({
|
||||||
|
url: 'locations',
|
||||||
|
crossOrigin: false,
|
||||||
|
type: 'json'
|
||||||
|
}, {
|
||||||
|
interval: 1 * 1000
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
var doFitBounds = true;
|
||||||
|
function refresh(e) {
|
||||||
|
var coordPart = function(v, dirs) {
|
||||||
|
return dirs.charAt(v >= 0 ? 0 : 1) +
|
||||||
|
(Math.round(Math.abs(v) * 100) / 100).toString();
|
||||||
|
},
|
||||||
|
popupContent = function(fId) {
|
||||||
|
var feature = e.features[fId],
|
||||||
|
c = feature.geometry.coordinates;
|
||||||
|
return feature.properties['name' ] + ' at ' + coordPart(c[1], 'NS') + ', ' + coordPart(c[0], 'EW');
|
||||||
|
},
|
||||||
|
bindFeaturePopup = function(fId) {
|
||||||
|
realtime.getLayer(fId).bindPopup(popupContent(fId));
|
||||||
|
},
|
||||||
|
updateFeaturePopup = function(fId) {
|
||||||
|
realtime.getLayer(fId).getPopup().setContent(popupContent(fId));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (doFitBounds) {
|
||||||
|
map.fitBounds(realtime.getBounds(), {maxZoom: 15});
|
||||||
|
doFitBounds = false;
|
||||||
|
}
|
||||||
|
Object.keys(e.enter).forEach(bindFeaturePopup);
|
||||||
|
Object.keys(e.update).forEach(updateFeaturePopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
realtime.on('update', refresh);
|
842
www/leaflet-realtime.js
Normal file
842
www/leaflet-realtime.js
Normal file
|
@ -0,0 +1,842 @@
|
||||||
|
!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),(f.L||(f.L={})).Realtime=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
|
||||||
|
/*!
|
||||||
|
* Reqwest! A general purpose XHR connection manager
|
||||||
|
* license MIT (c) Dustin Diaz 2014
|
||||||
|
* https://github.com/ded/reqwest
|
||||||
|
*/
|
||||||
|
|
||||||
|
!function (name, context, definition) {
|
||||||
|
if (typeof module != 'undefined' && module.exports) module.exports = definition()
|
||||||
|
else if (typeof define == 'function' && define.amd) define(definition)
|
||||||
|
else context[name] = definition()
|
||||||
|
}('reqwest', this, function () {
|
||||||
|
|
||||||
|
var win = window
|
||||||
|
, doc = document
|
||||||
|
, httpsRe = /^http/
|
||||||
|
, protocolRe = /(^\w+):\/\//
|
||||||
|
, twoHundo = /^(20\d|1223)$/ //http://stackoverflow.com/questions/10046972/msie-returns-status-code-of-1223-for-ajax-request
|
||||||
|
, byTag = 'getElementsByTagName'
|
||||||
|
, readyState = 'readyState'
|
||||||
|
, contentType = 'Content-Type'
|
||||||
|
, requestedWith = 'X-Requested-With'
|
||||||
|
, head = doc[byTag]('head')[0]
|
||||||
|
, uniqid = 0
|
||||||
|
, callbackPrefix = 'reqwest_' + (+new Date())
|
||||||
|
, lastValue // data stored by the most recent JSONP callback
|
||||||
|
, xmlHttpRequest = 'XMLHttpRequest'
|
||||||
|
, xDomainRequest = 'XDomainRequest'
|
||||||
|
, noop = function () {}
|
||||||
|
|
||||||
|
, isArray = typeof Array.isArray == 'function'
|
||||||
|
? Array.isArray
|
||||||
|
: function (a) {
|
||||||
|
return a instanceof Array
|
||||||
|
}
|
||||||
|
|
||||||
|
, defaultHeaders = {
|
||||||
|
'contentType': 'application/x-www-form-urlencoded'
|
||||||
|
, 'requestedWith': xmlHttpRequest
|
||||||
|
, 'accept': {
|
||||||
|
'*': 'text/javascript, text/html, application/xml, text/xml, */*'
|
||||||
|
, 'xml': 'application/xml, text/xml'
|
||||||
|
, 'html': 'text/html'
|
||||||
|
, 'text': 'text/plain'
|
||||||
|
, 'json': 'application/json, text/javascript'
|
||||||
|
, 'js': 'application/javascript, text/javascript'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
, xhr = function(o) {
|
||||||
|
// is it x-domain
|
||||||
|
if (o['crossOrigin'] === true) {
|
||||||
|
var xhr = win[xmlHttpRequest] ? new XMLHttpRequest() : null
|
||||||
|
if (xhr && 'withCredentials' in xhr) {
|
||||||
|
return xhr
|
||||||
|
} else if (win[xDomainRequest]) {
|
||||||
|
return new XDomainRequest()
|
||||||
|
} else {
|
||||||
|
throw new Error('Browser does not support cross-origin requests')
|
||||||
|
}
|
||||||
|
} else if (win[xmlHttpRequest]) {
|
||||||
|
return new XMLHttpRequest()
|
||||||
|
} else {
|
||||||
|
return new ActiveXObject('Microsoft.XMLHTTP')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
, globalSetupOptions = {
|
||||||
|
dataFilter: function (data) {
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function succeed(r) {
|
||||||
|
var protocol = protocolRe.exec(r.url);
|
||||||
|
protocol = (protocol && protocol[1]) || window.location.protocol;
|
||||||
|
return httpsRe.test(protocol) ? twoHundo.test(r.request.status) : !!r.request.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReadyState(r, success, error) {
|
||||||
|
return function () {
|
||||||
|
// use _aborted to mitigate against IE err c00c023f
|
||||||
|
// (can't read props on aborted request objects)
|
||||||
|
if (r._aborted) return error(r.request)
|
||||||
|
if (r._timedOut) return error(r.request, 'Request is aborted: timeout')
|
||||||
|
if (r.request && r.request[readyState] == 4) {
|
||||||
|
r.request.onreadystatechange = noop
|
||||||
|
if (succeed(r)) success(r.request)
|
||||||
|
else
|
||||||
|
error(r.request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHeaders(http, o) {
|
||||||
|
var headers = o['headers'] || {}
|
||||||
|
, h
|
||||||
|
|
||||||
|
headers['Accept'] = headers['Accept']
|
||||||
|
|| defaultHeaders['accept'][o['type']]
|
||||||
|
|| defaultHeaders['accept']['*']
|
||||||
|
|
||||||
|
var isAFormData = typeof FormData === 'function' && (o['data'] instanceof FormData);
|
||||||
|
// breaks cross-origin requests with legacy browsers
|
||||||
|
if (!o['crossOrigin'] && !headers[requestedWith]) headers[requestedWith] = defaultHeaders['requestedWith']
|
||||||
|
if (!headers[contentType] && !isAFormData) headers[contentType] = o['contentType'] || defaultHeaders['contentType']
|
||||||
|
for (h in headers)
|
||||||
|
headers.hasOwnProperty(h) && 'setRequestHeader' in http && http.setRequestHeader(h, headers[h])
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCredentials(http, o) {
|
||||||
|
if (typeof o['withCredentials'] !== 'undefined' && typeof http.withCredentials !== 'undefined') {
|
||||||
|
http.withCredentials = !!o['withCredentials']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generalCallback(data) {
|
||||||
|
lastValue = data
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlappend (url, s) {
|
||||||
|
return url + (/\?/.test(url) ? '&' : '?') + s
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleJsonp(o, fn, err, url) {
|
||||||
|
var reqId = uniqid++
|
||||||
|
, cbkey = o['jsonpCallback'] || 'callback' // the 'callback' key
|
||||||
|
, cbval = o['jsonpCallbackName'] || reqwest.getcallbackPrefix(reqId)
|
||||||
|
, cbreg = new RegExp('((^|\\?|&)' + cbkey + ')=([^&]+)')
|
||||||
|
, match = url.match(cbreg)
|
||||||
|
, script = doc.createElement('script')
|
||||||
|
, loaded = 0
|
||||||
|
, isIE10 = navigator.userAgent.indexOf('MSIE 10.0') !== -1
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
if (match[3] === '?') {
|
||||||
|
url = url.replace(cbreg, '$1=' + cbval) // wildcard callback func name
|
||||||
|
} else {
|
||||||
|
cbval = match[3] // provided callback func name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
url = urlappend(url, cbkey + '=' + cbval) // no callback details, add 'em
|
||||||
|
}
|
||||||
|
|
||||||
|
win[cbval] = generalCallback
|
||||||
|
|
||||||
|
script.type = 'text/javascript'
|
||||||
|
script.src = url
|
||||||
|
script.async = true
|
||||||
|
if (typeof script.onreadystatechange !== 'undefined' && !isIE10) {
|
||||||
|
// need this for IE due to out-of-order onreadystatechange(), binding script
|
||||||
|
// execution to an event listener gives us control over when the script
|
||||||
|
// is executed. See http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
|
||||||
|
script.htmlFor = script.id = '_reqwest_' + reqId
|
||||||
|
}
|
||||||
|
|
||||||
|
script.onload = script.onreadystatechange = function () {
|
||||||
|
if ((script[readyState] && script[readyState] !== 'complete' && script[readyState] !== 'loaded') || loaded) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
script.onload = script.onreadystatechange = null
|
||||||
|
script.onclick && script.onclick()
|
||||||
|
// Call the user callback with the last value stored and clean up values and scripts.
|
||||||
|
fn(lastValue)
|
||||||
|
lastValue = undefined
|
||||||
|
head.removeChild(script)
|
||||||
|
loaded = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the script to the DOM head
|
||||||
|
head.appendChild(script)
|
||||||
|
|
||||||
|
// Enable JSONP timeout
|
||||||
|
return {
|
||||||
|
abort: function () {
|
||||||
|
script.onload = script.onreadystatechange = null
|
||||||
|
err({}, 'Request is aborted: timeout', {})
|
||||||
|
lastValue = undefined
|
||||||
|
head.removeChild(script)
|
||||||
|
loaded = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRequest(fn, err) {
|
||||||
|
var o = this.o
|
||||||
|
, method = (o['method'] || 'GET').toUpperCase()
|
||||||
|
, url = typeof o === 'string' ? o : o['url']
|
||||||
|
// convert non-string objects to query-string form unless o['processData'] is false
|
||||||
|
, data = (o['processData'] !== false && o['data'] && typeof o['data'] !== 'string')
|
||||||
|
? reqwest.toQueryString(o['data'])
|
||||||
|
: (o['data'] || null)
|
||||||
|
, http
|
||||||
|
, sendWait = false
|
||||||
|
|
||||||
|
// if we're working on a GET request and we have data then we should append
|
||||||
|
// query string to end of URL and not post data
|
||||||
|
if ((o['type'] == 'jsonp' || method == 'GET') && data) {
|
||||||
|
url = urlappend(url, data)
|
||||||
|
data = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o['type'] == 'jsonp') return handleJsonp(o, fn, err, url)
|
||||||
|
|
||||||
|
// get the xhr from the factory if passed
|
||||||
|
// if the factory returns null, fall-back to ours
|
||||||
|
http = (o.xhr && o.xhr(o)) || xhr(o)
|
||||||
|
|
||||||
|
http.open(method, url, o['async'] === false ? false : true)
|
||||||
|
setHeaders(http, o)
|
||||||
|
setCredentials(http, o)
|
||||||
|
if (win[xDomainRequest] && http instanceof win[xDomainRequest]) {
|
||||||
|
http.onload = fn
|
||||||
|
http.onerror = err
|
||||||
|
// NOTE: see
|
||||||
|
// http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/30ef3add-767c-4436-b8a9-f1ca19b4812e
|
||||||
|
http.onprogress = function() {}
|
||||||
|
sendWait = true
|
||||||
|
} else {
|
||||||
|
http.onreadystatechange = handleReadyState(this, fn, err)
|
||||||
|
}
|
||||||
|
o['before'] && o['before'](http)
|
||||||
|
if (sendWait) {
|
||||||
|
setTimeout(function () {
|
||||||
|
http.send(data)
|
||||||
|
}, 200)
|
||||||
|
} else {
|
||||||
|
http.send(data)
|
||||||
|
}
|
||||||
|
return http
|
||||||
|
}
|
||||||
|
|
||||||
|
function Reqwest(o, fn) {
|
||||||
|
this.o = o
|
||||||
|
this.fn = fn
|
||||||
|
|
||||||
|
init.apply(this, arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setType(header) {
|
||||||
|
// json, javascript, text/plain, text/html, xml
|
||||||
|
if (header.match('json')) return 'json'
|
||||||
|
if (header.match('javascript')) return 'js'
|
||||||
|
if (header.match('text')) return 'html'
|
||||||
|
if (header.match('xml')) return 'xml'
|
||||||
|
}
|
||||||
|
|
||||||
|
function init(o, fn) {
|
||||||
|
|
||||||
|
this.url = typeof o == 'string' ? o : o['url']
|
||||||
|
this.timeout = null
|
||||||
|
|
||||||
|
// whether request has been fulfilled for purpose
|
||||||
|
// of tracking the Promises
|
||||||
|
this._fulfilled = false
|
||||||
|
// success handlers
|
||||||
|
this._successHandler = function(){}
|
||||||
|
this._fulfillmentHandlers = []
|
||||||
|
// error handlers
|
||||||
|
this._errorHandlers = []
|
||||||
|
// complete (both success and fail) handlers
|
||||||
|
this._completeHandlers = []
|
||||||
|
this._erred = false
|
||||||
|
this._responseArgs = {}
|
||||||
|
|
||||||
|
var self = this
|
||||||
|
|
||||||
|
fn = fn || function () {}
|
||||||
|
|
||||||
|
if (o['timeout']) {
|
||||||
|
this.timeout = setTimeout(function () {
|
||||||
|
timedOut()
|
||||||
|
}, o['timeout'])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o['success']) {
|
||||||
|
this._successHandler = function () {
|
||||||
|
o['success'].apply(o, arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o['error']) {
|
||||||
|
this._errorHandlers.push(function () {
|
||||||
|
o['error'].apply(o, arguments)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (o['complete']) {
|
||||||
|
this._completeHandlers.push(function () {
|
||||||
|
o['complete'].apply(o, arguments)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function complete (resp) {
|
||||||
|
o['timeout'] && clearTimeout(self.timeout)
|
||||||
|
self.timeout = null
|
||||||
|
while (self._completeHandlers.length > 0) {
|
||||||
|
self._completeHandlers.shift()(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function success (resp) {
|
||||||
|
var type = o['type'] || resp && setType(resp.getResponseHeader('Content-Type')) // resp can be undefined in IE
|
||||||
|
resp = (type !== 'jsonp') ? self.request : resp
|
||||||
|
// use global data filter on response text
|
||||||
|
var filteredResponse = globalSetupOptions.dataFilter(resp.responseText, type)
|
||||||
|
, r = filteredResponse
|
||||||
|
try {
|
||||||
|
resp.responseText = r
|
||||||
|
} catch (e) {
|
||||||
|
// can't assign this in IE<=8, just ignore
|
||||||
|
}
|
||||||
|
if (r) {
|
||||||
|
switch (type) {
|
||||||
|
case 'json':
|
||||||
|
try {
|
||||||
|
resp = win.JSON ? win.JSON.parse(r) : eval('(' + r + ')')
|
||||||
|
} catch (err) {
|
||||||
|
return error(resp, 'Could not parse JSON in response', err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'js':
|
||||||
|
resp = eval(r)
|
||||||
|
break
|
||||||
|
case 'html':
|
||||||
|
resp = r
|
||||||
|
break
|
||||||
|
case 'xml':
|
||||||
|
resp = resp.responseXML
|
||||||
|
&& resp.responseXML.parseError // IE trololo
|
||||||
|
&& resp.responseXML.parseError.errorCode
|
||||||
|
&& resp.responseXML.parseError.reason
|
||||||
|
? null
|
||||||
|
: resp.responseXML
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self._responseArgs.resp = resp
|
||||||
|
self._fulfilled = true
|
||||||
|
fn(resp)
|
||||||
|
self._successHandler(resp)
|
||||||
|
while (self._fulfillmentHandlers.length > 0) {
|
||||||
|
resp = self._fulfillmentHandlers.shift()(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
function timedOut() {
|
||||||
|
self._timedOut = true
|
||||||
|
self.request.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(resp, msg, t) {
|
||||||
|
resp = self.request
|
||||||
|
self._responseArgs.resp = resp
|
||||||
|
self._responseArgs.msg = msg
|
||||||
|
self._responseArgs.t = t
|
||||||
|
self._erred = true
|
||||||
|
while (self._errorHandlers.length > 0) {
|
||||||
|
self._errorHandlers.shift()(resp, msg, t)
|
||||||
|
}
|
||||||
|
complete(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.request = getRequest.call(this, success, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Reqwest.prototype = {
|
||||||
|
abort: function () {
|
||||||
|
this._aborted = true
|
||||||
|
this.request.abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
, retry: function () {
|
||||||
|
init.call(this, this.o, this.fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Small deviation from the Promises A CommonJs specification
|
||||||
|
* http://wiki.commonjs.org/wiki/Promises/A
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `then` will execute upon successful requests
|
||||||
|
*/
|
||||||
|
, then: function (success, fail) {
|
||||||
|
success = success || function () {}
|
||||||
|
fail = fail || function () {}
|
||||||
|
if (this._fulfilled) {
|
||||||
|
this._responseArgs.resp = success(this._responseArgs.resp)
|
||||||
|
} else if (this._erred) {
|
||||||
|
fail(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
||||||
|
} else {
|
||||||
|
this._fulfillmentHandlers.push(success)
|
||||||
|
this._errorHandlers.push(fail)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `always` will execute whether the request succeeds or fails
|
||||||
|
*/
|
||||||
|
, always: function (fn) {
|
||||||
|
if (this._fulfilled || this._erred) {
|
||||||
|
fn(this._responseArgs.resp)
|
||||||
|
} else {
|
||||||
|
this._completeHandlers.push(fn)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `fail` will execute when the request fails
|
||||||
|
*/
|
||||||
|
, fail: function (fn) {
|
||||||
|
if (this._erred) {
|
||||||
|
fn(this._responseArgs.resp, this._responseArgs.msg, this._responseArgs.t)
|
||||||
|
} else {
|
||||||
|
this._errorHandlers.push(fn)
|
||||||
|
}
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
, 'catch': function (fn) {
|
||||||
|
return this.fail(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reqwest(o, fn) {
|
||||||
|
return new Reqwest(o, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize newline variants according to spec -> CRLF
|
||||||
|
function normalize(s) {
|
||||||
|
return s ? s.replace(/\r?\n/g, '\r\n') : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function serial(el, cb) {
|
||||||
|
var n = el.name
|
||||||
|
, t = el.tagName.toLowerCase()
|
||||||
|
, optCb = function (o) {
|
||||||
|
// IE gives value="" even where there is no value attribute
|
||||||
|
// 'specified' ref: http://www.w3.org/TR/DOM-Level-3-Core/core.html#ID-862529273
|
||||||
|
if (o && !o['disabled'])
|
||||||
|
cb(n, normalize(o['attributes']['value'] && o['attributes']['value']['specified'] ? o['value'] : o['text']))
|
||||||
|
}
|
||||||
|
, ch, ra, val, i
|
||||||
|
|
||||||
|
// don't serialize elements that are disabled or without a name
|
||||||
|
if (el.disabled || !n) return
|
||||||
|
|
||||||
|
switch (t) {
|
||||||
|
case 'input':
|
||||||
|
if (!/reset|button|image|file/i.test(el.type)) {
|
||||||
|
ch = /checkbox/i.test(el.type)
|
||||||
|
ra = /radio/i.test(el.type)
|
||||||
|
val = el.value
|
||||||
|
// WebKit gives us "" instead of "on" if a checkbox has no value, so correct it here
|
||||||
|
;(!(ch || ra) || el.checked) && cb(n, normalize(ch && val === '' ? 'on' : val))
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'textarea':
|
||||||
|
cb(n, normalize(el.value))
|
||||||
|
break
|
||||||
|
case 'select':
|
||||||
|
if (el.type.toLowerCase() === 'select-one') {
|
||||||
|
optCb(el.selectedIndex >= 0 ? el.options[el.selectedIndex] : null)
|
||||||
|
} else {
|
||||||
|
for (i = 0; el.length && i < el.length; i++) {
|
||||||
|
el.options[i].selected && optCb(el.options[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect up all form elements found from the passed argument elements all
|
||||||
|
// the way down to child elements; pass a '<form>' or form fields.
|
||||||
|
// called with 'this'=callback to use for serial() on each element
|
||||||
|
function eachFormElement() {
|
||||||
|
var cb = this
|
||||||
|
, e, i
|
||||||
|
, serializeSubtags = function (e, tags) {
|
||||||
|
var i, j, fa
|
||||||
|
for (i = 0; i < tags.length; i++) {
|
||||||
|
fa = e[byTag](tags[i])
|
||||||
|
for (j = 0; j < fa.length; j++) serial(fa[j], cb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < arguments.length; i++) {
|
||||||
|
e = arguments[i]
|
||||||
|
if (/input|select|textarea/i.test(e.tagName)) serial(e, cb)
|
||||||
|
serializeSubtags(e, [ 'input', 'select', 'textarea' ])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// standard query string style serialization
|
||||||
|
function serializeQueryString() {
|
||||||
|
return reqwest.toQueryString(reqwest.serializeArray.apply(null, arguments))
|
||||||
|
}
|
||||||
|
|
||||||
|
// { 'name': 'value', ... } style serialization
|
||||||
|
function serializeHash() {
|
||||||
|
var hash = {}
|
||||||
|
eachFormElement.apply(function (name, value) {
|
||||||
|
if (name in hash) {
|
||||||
|
hash[name] && !isArray(hash[name]) && (hash[name] = [hash[name]])
|
||||||
|
hash[name].push(value)
|
||||||
|
} else hash[name] = value
|
||||||
|
}, arguments)
|
||||||
|
return hash
|
||||||
|
}
|
||||||
|
|
||||||
|
// [ { name: 'name', value: 'value' }, ... ] style serialization
|
||||||
|
reqwest.serializeArray = function () {
|
||||||
|
var arr = []
|
||||||
|
eachFormElement.apply(function (name, value) {
|
||||||
|
arr.push({name: name, value: value})
|
||||||
|
}, arguments)
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
reqwest.serialize = function () {
|
||||||
|
if (arguments.length === 0) return ''
|
||||||
|
var opt, fn
|
||||||
|
, args = Array.prototype.slice.call(arguments, 0)
|
||||||
|
|
||||||
|
opt = args.pop()
|
||||||
|
opt && opt.nodeType && args.push(opt) && (opt = null)
|
||||||
|
opt && (opt = opt.type)
|
||||||
|
|
||||||
|
if (opt == 'map') fn = serializeHash
|
||||||
|
else if (opt == 'array') fn = reqwest.serializeArray
|
||||||
|
else fn = serializeQueryString
|
||||||
|
|
||||||
|
return fn.apply(null, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqwest.toQueryString = function (o, trad) {
|
||||||
|
var prefix, i
|
||||||
|
, traditional = trad || false
|
||||||
|
, s = []
|
||||||
|
, enc = encodeURIComponent
|
||||||
|
, add = function (key, value) {
|
||||||
|
// If value is a function, invoke it and return its value
|
||||||
|
value = ('function' === typeof value) ? value() : (value == null ? '' : value)
|
||||||
|
s[s.length] = enc(key) + '=' + enc(value)
|
||||||
|
}
|
||||||
|
// If an array was passed in, assume that it is an array of form elements.
|
||||||
|
if (isArray(o)) {
|
||||||
|
for (i = 0; o && i < o.length; i++) add(o[i]['name'], o[i]['value'])
|
||||||
|
} else {
|
||||||
|
// If traditional, encode the "old" way (the way 1.3.2 or older
|
||||||
|
// did it), otherwise encode params recursively.
|
||||||
|
for (prefix in o) {
|
||||||
|
if (o.hasOwnProperty(prefix)) buildParams(prefix, o[prefix], traditional, add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// spaces should be + according to spec
|
||||||
|
return s.join('&').replace(/%20/g, '+')
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParams(prefix, obj, traditional, add) {
|
||||||
|
var name, i, v
|
||||||
|
, rbracket = /\[\]$/
|
||||||
|
|
||||||
|
if (isArray(obj)) {
|
||||||
|
// Serialize array item.
|
||||||
|
for (i = 0; obj && i < obj.length; i++) {
|
||||||
|
v = obj[i]
|
||||||
|
if (traditional || rbracket.test(prefix)) {
|
||||||
|
// Treat each array item as a scalar.
|
||||||
|
add(prefix, v)
|
||||||
|
} else {
|
||||||
|
buildParams(prefix + '[' + (typeof v === 'object' ? i : '') + ']', v, traditional, add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (obj && obj.toString() === '[object Object]') {
|
||||||
|
// Serialize object item.
|
||||||
|
for (name in obj) {
|
||||||
|
buildParams(prefix + '[' + name + ']', obj[name], traditional, add)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Serialize scalar item.
|
||||||
|
add(prefix, obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqwest.getcallbackPrefix = function () {
|
||||||
|
return callbackPrefix
|
||||||
|
}
|
||||||
|
|
||||||
|
// jQuery and Zepto compatibility, differences can be remapped here so you can call
|
||||||
|
// .ajax.compat(options, callback)
|
||||||
|
reqwest.compat = function (o, fn) {
|
||||||
|
if (o) {
|
||||||
|
o['type'] && (o['method'] = o['type']) && delete o['type']
|
||||||
|
o['dataType'] && (o['type'] = o['dataType'])
|
||||||
|
o['jsonpCallback'] && (o['jsonpCallbackName'] = o['jsonpCallback']) && delete o['jsonpCallback']
|
||||||
|
o['jsonp'] && (o['jsonpCallback'] = o['jsonp'])
|
||||||
|
}
|
||||||
|
return new Reqwest(o, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
reqwest.ajaxSetup = function (options) {
|
||||||
|
options = options || {}
|
||||||
|
for (var k in options) {
|
||||||
|
globalSetupOptions[k] = options[k]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqwest
|
||||||
|
});
|
||||||
|
|
||||||
|
},{}],2:[function(require,module,exports){
|
||||||
|
(function (global){
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var L = (typeof window !== "undefined" ? window.L : typeof global !== "undefined" ? global.L : null),
|
||||||
|
reqwest = require('reqwest');
|
||||||
|
|
||||||
|
L.Realtime = L.GeoJSON.extend({
|
||||||
|
includes: L.Mixin.Events,
|
||||||
|
|
||||||
|
options: {
|
||||||
|
start: true,
|
||||||
|
interval: 60 * 1000,
|
||||||
|
getFeatureId: function(f) {
|
||||||
|
return f.properties.id;
|
||||||
|
},
|
||||||
|
updateFeature: function(f, oldLayer, newLayer) {
|
||||||
|
if (f.geometry.type === 'Point') {
|
||||||
|
oldLayer.setLatLng(newLayer.getLatLng());
|
||||||
|
return oldLayer;
|
||||||
|
} else {
|
||||||
|
return newLayer;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cache: false
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function(src, options) {
|
||||||
|
L.GeoJSON.prototype.initialize.call(this, undefined, options);
|
||||||
|
|
||||||
|
if (typeof(src) === 'function') {
|
||||||
|
this._src = src;
|
||||||
|
} else {
|
||||||
|
this._src = L.bind(function(responseHandler, errorHandler) {
|
||||||
|
var reqOptions = this.options.cache ? src : this._bustCache(src);
|
||||||
|
|
||||||
|
reqwest(reqOptions).then(responseHandler, errorHandler);
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._features = {};
|
||||||
|
this._featureLayers = {};
|
||||||
|
|
||||||
|
if (this.options.start) {
|
||||||
|
this.start();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
start: function() {
|
||||||
|
if (!this._timer) {
|
||||||
|
this._timer = setInterval(L.bind(this.update, this),
|
||||||
|
this.options.interval);
|
||||||
|
this.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
stop: function() {
|
||||||
|
if (this._timer) {
|
||||||
|
clearTimeout(this._timer);
|
||||||
|
delete this._timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
isRunning: function() {
|
||||||
|
return this._timer;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: function(geojson) {
|
||||||
|
var responseHandler,
|
||||||
|
errorHandler;
|
||||||
|
|
||||||
|
if (geojson) {
|
||||||
|
this._onNewData(false, geojson);
|
||||||
|
} else {
|
||||||
|
responseHandler = L.bind(function(data) { this._onNewData(true, data); }, this);
|
||||||
|
errorHandler = L.bind(this._onError, this);
|
||||||
|
|
||||||
|
this._src(responseHandler, errorHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
remove: function(geojson) {
|
||||||
|
var features = L.Util.isArray(geojson) ? geojson : geojson.features ? geojson.features : [geojson],
|
||||||
|
exit = {},
|
||||||
|
i,
|
||||||
|
len,
|
||||||
|
fId;
|
||||||
|
|
||||||
|
for (i = 0, len = features.length; i < len; i++) {
|
||||||
|
fId = this.options.getFeatureId(features[i]);
|
||||||
|
this.removeLayer(this._featureLayers[fId]);
|
||||||
|
exit[fId] = this._features[fId];
|
||||||
|
delete this._features[fId];
|
||||||
|
delete this._featureLayers[fId];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fire('update', {
|
||||||
|
features: this._features,
|
||||||
|
enter: {},
|
||||||
|
update: {},
|
||||||
|
exit: exit
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLayer: function(featureId) {
|
||||||
|
return this._featureLayers[featureId];
|
||||||
|
},
|
||||||
|
|
||||||
|
getFeature: function(featureId) {
|
||||||
|
return this._features[featureId];
|
||||||
|
},
|
||||||
|
|
||||||
|
_onNewData: function(removeMissing, response) {
|
||||||
|
var oef = this.options.onEachFeature,
|
||||||
|
layersToRemove = [],
|
||||||
|
features = {},
|
||||||
|
enter = {},
|
||||||
|
update = {},
|
||||||
|
exit = {},
|
||||||
|
i;
|
||||||
|
|
||||||
|
this.options.onEachFeature = L.bind(function onEachFeature(f, l) {
|
||||||
|
var fId,
|
||||||
|
oldLayer,
|
||||||
|
newLayer;
|
||||||
|
|
||||||
|
if (oef) {
|
||||||
|
oef(f, l);
|
||||||
|
}
|
||||||
|
|
||||||
|
fId = this.options.getFeatureId(f);
|
||||||
|
oldLayer = this._featureLayers[fId];
|
||||||
|
|
||||||
|
if (oldLayer) {
|
||||||
|
newLayer = this.options.updateFeature(f, oldLayer, l);
|
||||||
|
if (newLayer !== oldLayer) {
|
||||||
|
this.removeLayer(oldLayer);
|
||||||
|
}
|
||||||
|
if (newLayer !== l) {
|
||||||
|
layersToRemove.push(l);
|
||||||
|
}
|
||||||
|
|
||||||
|
l = newLayer;
|
||||||
|
update[fId] = f;
|
||||||
|
} else {
|
||||||
|
enter[fId] = f;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._featureLayers[fId] = l;
|
||||||
|
this._features[fId] = features[fId] = f;
|
||||||
|
}, this);
|
||||||
|
|
||||||
|
this.addData(response);
|
||||||
|
|
||||||
|
if (removeMissing) {
|
||||||
|
exit = this._removeUnknown(features);
|
||||||
|
}
|
||||||
|
for (i = 0; i < layersToRemove.length; i++) {
|
||||||
|
this.removeLayer(layersToRemove[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fire('update', {
|
||||||
|
features: this._features,
|
||||||
|
enter: enter,
|
||||||
|
update: update,
|
||||||
|
exit: exit
|
||||||
|
});
|
||||||
|
|
||||||
|
this.options.onEachFeature = oef;
|
||||||
|
},
|
||||||
|
|
||||||
|
_onError: function(err, msg) {
|
||||||
|
this.fire('error', {
|
||||||
|
error: err,
|
||||||
|
message: msg
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_removeUnknown: function(known) {
|
||||||
|
var fId,
|
||||||
|
removed = {};
|
||||||
|
for (fId in this._featureLayers) {
|
||||||
|
if (!known[fId]) {
|
||||||
|
this.removeLayer(this._featureLayers[fId]);
|
||||||
|
removed[fId] = this._features[fId];
|
||||||
|
delete this._featureLayers[fId];
|
||||||
|
delete this._features[fId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
},
|
||||||
|
|
||||||
|
_bustCache: function(src) {
|
||||||
|
function fixUrl(url) {
|
||||||
|
return url + L.Util.getParamString({'_': new Date().getTime()});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof src === 'string' || src instanceof String) {
|
||||||
|
return fixUrl(src);
|
||||||
|
} else {
|
||||||
|
return L.extend({}, src, {url: fixUrl(src.url)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.realtime = function(src, options) {
|
||||||
|
return new L.Realtime(src, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Realtime.reqwest = reqwest;
|
||||||
|
|
||||||
|
module.exports = L.Realtime;
|
||||||
|
|
||||||
|
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
|
||||||
|
},{"reqwest":1}]},{},[2])(2)
|
||||||
|
});
|
9180
www/leaflet-src.js
Normal file
9180
www/leaflet-src.js
Normal file
File diff suppressed because it is too large
Load diff
478
www/leaflet.css
Normal file
478
www/leaflet.css
Normal file
|
@ -0,0 +1,478 @@
|
||||||
|
/* required styles */
|
||||||
|
|
||||||
|
.leaflet-map-pane,
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow,
|
||||||
|
.leaflet-tile-pane,
|
||||||
|
.leaflet-tile-container,
|
||||||
|
.leaflet-overlay-pane,
|
||||||
|
.leaflet-shadow-pane,
|
||||||
|
.leaflet-marker-pane,
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-overlay-pane svg,
|
||||||
|
.leaflet-zoom-box,
|
||||||
|
.leaflet-image-layer,
|
||||||
|
.leaflet-layer {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
overflow: hidden;
|
||||||
|
-ms-touch-action: none;
|
||||||
|
}
|
||||||
|
.leaflet-tile,
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
.leaflet-marker-icon,
|
||||||
|
.leaflet-marker-shadow {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* map is broken in FF if you have max-width: 100% on tiles */
|
||||||
|
.leaflet-container img {
|
||||||
|
max-width: none !important;
|
||||||
|
}
|
||||||
|
/* stupid Android 2 doesn't understand "max-width: none" properly */
|
||||||
|
.leaflet-container img.leaflet-image-layer {
|
||||||
|
max-width: 15000px !important;
|
||||||
|
}
|
||||||
|
.leaflet-tile {
|
||||||
|
filter: inherit;
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-tile-loaded {
|
||||||
|
visibility: inherit;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
|
||||||
|
.leaflet-overlay-pane svg {
|
||||||
|
-moz-user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-tile-pane { z-index: 2; }
|
||||||
|
.leaflet-objects-pane { z-index: 3; }
|
||||||
|
.leaflet-overlay-pane { z-index: 4; }
|
||||||
|
.leaflet-shadow-pane { z-index: 5; }
|
||||||
|
.leaflet-marker-pane { z-index: 6; }
|
||||||
|
.leaflet-popup-pane { z-index: 7; }
|
||||||
|
|
||||||
|
.leaflet-vml-shape {
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
.lvml {
|
||||||
|
behavior: url(#default#VML);
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* control positioning */
|
||||||
|
|
||||||
|
.leaflet-control {
|
||||||
|
position: relative;
|
||||||
|
z-index: 7;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.leaflet-top,
|
||||||
|
.leaflet-bottom {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.leaflet-top {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
.leaflet-right {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.leaflet-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
.leaflet-left {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control {
|
||||||
|
float: left;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.leaflet-top .leaflet-control {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.leaflet-right .leaflet-control {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom and fade animations */
|
||||||
|
|
||||||
|
.leaflet-fade-anim .leaflet-tile,
|
||||||
|
.leaflet-fade-anim .leaflet-popup {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity 0.2s linear;
|
||||||
|
-moz-transition: opacity 0.2s linear;
|
||||||
|
-o-transition: opacity 0.2s linear;
|
||||||
|
transition: opacity 0.2s linear;
|
||||||
|
}
|
||||||
|
.leaflet-fade-anim .leaflet-tile-loaded,
|
||||||
|
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||||
|
}
|
||||||
|
.leaflet-zoom-anim .leaflet-tile,
|
||||||
|
.leaflet-pan-anim .leaflet-tile,
|
||||||
|
.leaflet-touching .leaflet-zoom-animated {
|
||||||
|
-webkit-transition: none;
|
||||||
|
-moz-transition: none;
|
||||||
|
-o-transition: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* cursors */
|
||||||
|
|
||||||
|
.leaflet-clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.leaflet-container {
|
||||||
|
cursor: -webkit-grab;
|
||||||
|
cursor: -moz-grab;
|
||||||
|
}
|
||||||
|
.leaflet-popup-pane,
|
||||||
|
.leaflet-control {
|
||||||
|
cursor: auto;
|
||||||
|
}
|
||||||
|
.leaflet-dragging .leaflet-container,
|
||||||
|
.leaflet-dragging .leaflet-clickable {
|
||||||
|
cursor: move;
|
||||||
|
cursor: -webkit-grabbing;
|
||||||
|
cursor: -moz-grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* visual tweaks */
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #ddd;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
.leaflet-container a {
|
||||||
|
color: #0078A8;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-active {
|
||||||
|
outline: 2px solid orange;
|
||||||
|
}
|
||||||
|
.leaflet-zoom-box {
|
||||||
|
border: 2px dotted #38f;
|
||||||
|
background: rgba(255,255,255,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general typography */
|
||||||
|
.leaflet-container {
|
||||||
|
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* general toolbar styles */
|
||||||
|
|
||||||
|
.leaflet-bar {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-bar a:hover {
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
line-height: 26px;
|
||||||
|
display: block;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.leaflet-bar a,
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-position: 50% 50%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:hover {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:first-child {
|
||||||
|
border-top-left-radius: 4px;
|
||||||
|
border-top-right-radius: 4px;
|
||||||
|
}
|
||||||
|
.leaflet-bar a:last-child {
|
||||||
|
border-bottom-left-radius: 4px;
|
||||||
|
border-bottom-right-radius: 4px;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.leaflet-bar a.leaflet-disabled {
|
||||||
|
cursor: default;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
color: #bbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-bar a {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
line-height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* zoom control */
|
||||||
|
|
||||||
|
.leaflet-control-zoom-in,
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||||
|
text-indent: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-zoom-out {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-zoom-in {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-zoom-out {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* layers control */
|
||||||
|
|
||||||
|
.leaflet-control-layers {
|
||||||
|
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers.png);
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
.leaflet-retina .leaflet-control-layers-toggle {
|
||||||
|
background-image: url(images/layers-2x.png);
|
||||||
|
background-size: 26px 26px;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers-toggle {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers .leaflet-control-layers-list,
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-expanded {
|
||||||
|
padding: 6px 10px 6px 6px;
|
||||||
|
color: #333;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-selector {
|
||||||
|
margin-top: 2px;
|
||||||
|
position: relative;
|
||||||
|
top: 1px;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.leaflet-control-layers-separator {
|
||||||
|
height: 0;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 5px -10px 5px -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* attribution and scale controls */
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-control-attribution {
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.7);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution,
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.leaflet-control-attribution a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
.leaflet-container .leaflet-control-attribution,
|
||||||
|
.leaflet-container .leaflet-control-scale {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
.leaflet-left .leaflet-control-scale {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-bottom .leaflet-control-scale {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line {
|
||||||
|
border: 2px solid #777;
|
||||||
|
border-top: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 2px 5px 1px;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
|
||||||
|
background: #fff;
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child) {
|
||||||
|
border-top: 2px solid #777;
|
||||||
|
border-bottom: none;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||||
|
border-bottom: 2px solid #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-touch .leaflet-control-attribution,
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.leaflet-touch .leaflet-control-layers,
|
||||||
|
.leaflet-touch .leaflet-bar {
|
||||||
|
border: 2px solid rgba(0,0,0,0.2);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* popup */
|
||||||
|
|
||||||
|
.leaflet-popup {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
padding: 1px;
|
||||||
|
text-align: left;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 13px 19px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.leaflet-popup-content p {
|
||||||
|
margin: 18px 0;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip-container {
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 40px;
|
||||||
|
height: 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 1px;
|
||||||
|
|
||||||
|
margin: -10px auto 0;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-moz-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
-o-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
.leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 4px 4px 0 0;
|
||||||
|
text-align: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 14px;
|
||||||
|
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||||
|
color: #c3c3c3;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.leaflet-popup-scrolled {
|
||||||
|
overflow: auto;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper {
|
||||||
|
zoom: 1;
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
width: 24px;
|
||||||
|
margin: 0 auto;
|
||||||
|
|
||||||
|
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||||
|
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||||
|
}
|
||||||
|
.leaflet-oldie .leaflet-popup-tip-container {
|
||||||
|
margin-top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-oldie .leaflet-control-zoom,
|
||||||
|
.leaflet-oldie .leaflet-control-layers,
|
||||||
|
.leaflet-oldie .leaflet-popup-content-wrapper,
|
||||||
|
.leaflet-oldie .leaflet-popup-tip {
|
||||||
|
border: 1px solid #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* div icon */
|
||||||
|
|
||||||
|
.leaflet-div-icon {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #666;
|
||||||
|
}
|
16
www/site.css
Normal file
16
www/site.css
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
|
||||||
|
|
||||||
|
.leaflet-marker-icon, .leaflet-marker-shadow {
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
-webkit-transition: transform 0.25s ease;
|
||||||
|
-o-transition: transform 0.25s ease;
|
||||||
|
-moz-transition: transform 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
Loading…
Reference in a new issue