First prototype

This commit is contained in:
Felix Niederwanger 2021-05-24 15:51:04 +02:00
parent 52e6e094d5
commit fba8cc058e
Signed by: phoenix
GPG key ID: 6E77A590E3F6D71C
19 changed files with 11063 additions and 7 deletions

3
.gitignore vendored
View file

@ -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
View 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
View 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

View file

@ -1,7 +0,0 @@
package main
import "fmt"
func main() {
fmt.Println("ot-browser")
}

22
cmd/ot-browser/geojson.go Normal file
View 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
}

View 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)
}

View 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
View 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
View file

@ -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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 797 B

15
www/index.html Normal file
View 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
View 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: '&copy; <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
View 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

File diff suppressed because it is too large Load diff

478
www/leaflet.css Normal file
View 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
View 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;
}