First prototype
This commit is contained in:
parent
52e6e094d5
commit
fba8cc058e
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,6 +5,7 @@
|
|||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
/ot-browser
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
@ -15,3 +16,5 @@
|
|||
# Dependency directories (remove the comment below to include it)
|
||||
# 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
|
||||
|
||||
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