commit 200dd620ae5ff315e0d9ddd7cb1405bc6c3d6817 Author: gidsi Date: Fri Apr 20 18:08:55 2018 +0200 merge eva repos into single repository diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ee7d6a5 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,14 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. + diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..dd3bed1 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,2 @@ +eva-backend +.idea diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..d19ccf4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:alpine as builder +RUN apk --no-cache add git +WORKDIR /go/src/app +COPY . . +RUN go get -d -v ./... +RUN go install -v ./... + + +FROM alpine:latest +RUN apk --no-cache add ca-certificates +WORKDIR /app +COPY --from=builder /go/bin/app . +COPY config.yaml config.yaml +EXPOSE 8080 +CMD ["./app", "-shared_secret=$SHARED_SECRET"] diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..e655e6f --- /dev/null +++ b/backend/README.md @@ -0,0 +1,15 @@ +eva-backend +=========== + +See yaml file for database config + +```bash +go get +go install +SHARED_SECRET=foo eva-backend +``` + +SpaceAPI extensions +=================== + +* Key `ext_ccc` describes Chaos Computer Club relation status (Example values: `"chaostreff"` or `"erfa"`) diff --git a/backend/calendar.go b/backend/calendar.go new file mode 100644 index 0000000..0653e02 --- /dev/null +++ b/backend/calendar.go @@ -0,0 +1,47 @@ +package main + +import ( + "github.com/gidsi/ics-golang" + "log" +) + +func getCalendars() { + spaceDataArray := readSpacedata() + + for _, spaceData := range spaceDataArray { + parser := ics.New() + parserChan := parser.GetInputChan() + parserChan <- spaceData.Feeds.Calendar.Url + outputChan := parser.GetOutputChan() + calendar := Calendar{} + calendar.Space = spaceData.Space + events := []Event{} + go func() { + for event := range outputChan { + events = append(events, mapEventObject(event)) + } + }() + parser.Wait() + calendar.Events = events + writeCalendar(calendar) + log.Println("calendar write done for " + spaceData.Space) + } +} + +func mapEventObject(event *ics.Event) Event { + eventData := Event{} + + eventData.Start = event.GetStart() + eventData.ImportedId = event.GetImportedID() + eventData.Status = event.GetStatus() + eventData.Description = event.GetDescription() + eventData.Location = event.GetLocation() + eventData.Summary = event.GetSummary() + eventData.Rrule = event.GetRRule() + eventData.Url = event.GetUrl() + eventData.Class = event.GetClass() + eventData.Sequence = event.GetSequence() + eventData.WholeDayEvent = event.GetWholeDayEvent() + + return eventData +} \ No newline at end of file diff --git a/backend/config.yaml b/backend/config.yaml new file mode 100644 index 0000000..7e7689c --- /dev/null +++ b/backend/config.yaml @@ -0,0 +1,3 @@ +--- +mongodb_server: database +mongodb_database: eva diff --git a/backend/configFile.go b/backend/configFile.go new file mode 100644 index 0000000..16e587c --- /dev/null +++ b/backend/configFile.go @@ -0,0 +1,7 @@ +package main + +type ConfigFile struct { + SharedSecret string `yaml:"shared_secret,omitempty"` + MongoDbServer string `yaml:"mongodb_server,omitempty"` + MongoDbDatabase string `yaml:"mongodb_database,omitempty"` +} \ No newline at end of file diff --git a/backend/database.go b/backend/database.go new file mode 100644 index 0000000..a88df60 --- /dev/null +++ b/backend/database.go @@ -0,0 +1,115 @@ +package main + +import ( + "log" + "gopkg.in/mgo.v2" + "gopkg.in/mgo.v2/bson" +) + +func writeSpaceData(data SpaceData) { + if(data.Space != "") { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("spacedata") + _, err = c.Upsert(bson.M{ "space": data.Space }, data) + if err != nil { + log.Fatal(err) + } + } +} + +func writeSpaceurl(spaceUrl SpaceUrl) { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("spaceurl") + count, _ := c.Find(bson.M{ "url": spaceUrl.Url }).Count() + if(count == 0) { + c.Insert(spaceUrl); + } +} + +func writeCalendar(calendar Calendar) { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("calendar") + c.Upsert(bson.M{ "space": calendar.Space }, calendar) +} + +func updateSpaceurl(spaceUrl SpaceUrl) { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("spaceurl") + c.Update(bson.M{ "url": spaceUrl.Url }, spaceUrl); +} + +func readSpacedata() []SpaceData { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("spacedata") + result := []SpaceData{} + c.Find(bson.M{}).Iter().All(&result) + + return result +} + +func readSpaceurl() []SpaceUrl { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("spaceurl") + result := []SpaceUrl{} + c.Find(bson.M{}).Iter().All(&result) + + return result +} + +func readCalendar() []Calendar { + session, err := mgo.Dial(config.MongoDbServer) + if err != nil { + panic(err) + } + defer session.Close() + + session.SetMode(mgo.Monotonic, true) + + c := session.DB(config.MongoDbDatabase).C("calendar") + result := []Calendar{} + c.Find(bson.M{}).Iter().All(&result) + + return result +} \ No newline at end of file diff --git a/backend/eva-backend.go b/backend/eva-backend.go new file mode 100644 index 0000000..a661b06 --- /dev/null +++ b/backend/eva-backend.go @@ -0,0 +1,92 @@ +package main + +import ( + "net/http" + "encoding/json" + "gopkg.in/yaml.v2" + "log" + "io/ioutil" + "os" + "time" + "github.com/gorilla/mux" +) + +var config = ConfigFile{} + +func main() { + data, _ := ioutil.ReadFile("config.yaml") + yaml.Unmarshal(data, &config) + config.SharedSecret = os.Getenv("SHARED_SECRET") + + router := NewRouter() + http.Handle("/", router) + log.Fatal(http.ListenAndServe(":8080", router)) +} + +func getJson(url string, target interface{}) error { + r, err := http.Get(url) + if err != nil { + return err + } + defer r.Body.Close() + + return json.NewDecoder(r.Body).Decode(target) +} + +func SpaceDataIndex(w http.ResponseWriter, r *http.Request) { + ReturnJson(w, readSpacedata()) +} + +func SpaceUrlIndex(w http.ResponseWriter, r *http.Request) { + ReturnJson(w, readSpaceurl()) +} + +func CalendarIndex(w http.ResponseWriter, r *http.Request) { + ReturnJson(w, readCalendar()) +} + +func SpaceUrlAdd(w http.ResponseWriter, r *http.Request) { + spaceUrl := SpaceUrl{} + createEntry(&spaceUrl, w, r) + spaceUrl.Validated = false + writeSpaceurl(spaceUrl) +} + +func SpaceUrlUpdate(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + SharedSecret := vars["SharedSecret"] + if(SharedSecret == config.SharedSecret) { + spaceUrl := SpaceUrl{} + createEntry(&spaceUrl, w, r) + updateSpaceurl(spaceUrl) + } +} + +func loadSpaceData() { + spaceUrls := readSpaceurl() + + timestamp := time.Now().Unix() + + for _, spaceUrl := range spaceUrls { + if(spaceUrl.Validated && int64(spaceUrl.LastUpdated + 60) < timestamp) { + spaceData := SpaceData{} + err := getJson(spaceUrl.Url, &spaceData) + if(err != nil) { + log.Println(spaceUrl.Url) + log.Println(err) + } else { + writeSpaceData(spaceData) + + spaceUrl.LastUpdated = timestamp + updateSpaceurl(spaceUrl) + } + } + } +} + +func refreshData(w http.ResponseWriter, r *http.Request) { + loadSpaceData() + getCalendars() + + w.WriteHeader(204) +} \ No newline at end of file diff --git a/backend/event.go b/backend/event.go new file mode 100644 index 0000000..b29c262 --- /dev/null +++ b/backend/event.go @@ -0,0 +1,23 @@ +package main + +import "time" + +type Calendar struct { + Space string + Events []Event +} + +type Event struct { + Start time.Time `json:"start"` + ImportedId string `json:"importId"` + Status string `json:"status"` + Description string `json:"description"` + Location string `json:"location"` + Summary string `json:"summary"` + Rrule string `json:"rrule"` + Class string `json:"class"` + Url string `json:"url"` + Sequence int `json:"sequence"` + WholeDayEvent bool `json:"whileDayEvent"` + +} \ No newline at end of file diff --git a/backend/logger.go b/backend/logger.go new file mode 100644 index 0000000..94dfecf --- /dev/null +++ b/backend/logger.go @@ -0,0 +1,29 @@ +package main + +import ( + "log" + "net/http" + "time" +) + +func Logger(inner http.Handler, name string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + inner.ServeHTTP(w, r) + + log.Printf( + "%s\t%s\t%s\t%s", + r.Method, + r.RequestURI, + name, + time.Since(start), + ) + }) +} + +func logError(err error) { + if err != nil { + log.Fatal(err) + } +} \ No newline at end of file diff --git a/backend/response.go b/backend/response.go new file mode 100644 index 0000000..2bf1825 --- /dev/null +++ b/backend/response.go @@ -0,0 +1,48 @@ +package main + +import ( + "net/http" + "encoding/json" + "fmt" +) + +type DataObject interface { + Save() DataObject +} + +func Index(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "Hello? Yes, this is dog!") +} + +func ReturnJson(w http.ResponseWriter, v interface{}) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "content-type") + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(v); err != nil { + panic(err) + } +} + +func ReturnDataNotFound(w http.ResponseWriter) { + w.WriteHeader(http.StatusNotFound) +} + +func createEntry(i interface{}, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE") + w.Header().Set("Access-Control-Allow-Headers", "content-type") + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + if err := json.NewDecoder(r.Body).Decode(i); err != nil { + w.WriteHeader(422) // unprocessable entity + if err := json.NewEncoder(w).Encode(err); err != nil { + panic(err) + } + } else { + w.WriteHeader(http.StatusCreated) + if err := json.NewEncoder(w).Encode(i); err != nil { + panic(err) + } + } +} \ No newline at end of file diff --git a/backend/router.go b/backend/router.go new file mode 100644 index 0000000..125d101 --- /dev/null +++ b/backend/router.go @@ -0,0 +1,51 @@ +package main + +import ( + "net/http" + "github.com/gorilla/mux" + "log" +) + +func NewRouter() *mux.Router { + router := mux.NewRouter().StrictSlash(true) + for _, route := range IndexRoutes { + var handler http.Handler + handler = Logger(route.Handler, route.Name) + + router. + Methods(route.Method). + Path(route.Pattern). + Name(route.Name). + Handler(handler) + + router. + Methods("OPTIONS"). + Name("Options Handler"). + Handler(http.HandlerFunc(optionsHandler)) + } + + router. + Methods("OPTIONS"). + Name("Options Handler"). + Handler(http.HandlerFunc(optionsHandler)) + + router.NotFoundHandler = http.HandlerFunc(notFound) + + return router +} + +func notFound(w http.ResponseWriter, r *http.Request) { + log.Print("---------------------------------") + log.Print(w) + log.Print("---------------------------------") + log.Print(r) + log.Print("---------------------------------") + w.WriteHeader(http.StatusNotFound) +} + +func optionsHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE") + w.WriteHeader(200) +} \ No newline at end of file diff --git a/backend/routes.go b/backend/routes.go new file mode 100644 index 0000000..5e3f85e --- /dev/null +++ b/backend/routes.go @@ -0,0 +1,58 @@ +package main + +import "net/http" + +type Route struct { + Name string + Method string + Pattern string + Handler http.HandlerFunc +} + + +type Routes []Route + +var IndexRoutes = Routes{ + Route{ + "Index", + "GET", + "/", + Index, + }, + Route{ + "SpaceDataIndex", + "GET", + "/spaces", + SpaceDataIndex, + }, + Route{ + "SpaceUrlIndex", + "GET", + "/urls", + SpaceUrlIndex, + }, + Route{ + "CalendarIndex", + "GET", + "/calendar", + CalendarIndex, + }, + Route{ + "SpaceUrlAdd", + "POST", + "/urls", + SpaceUrlAdd, + }, + Route{ + "SpaceUrlUpdate", + "PUT", + "/urls/{SharedSecret}", + SpaceUrlUpdate, + }, + Route{ + "RefreshData", + "GET", + "/refresh", + refreshData, + }, +} \ No newline at end of file diff --git a/backend/spacedata.go b/backend/spacedata.go new file mode 100644 index 0000000..26d268f --- /dev/null +++ b/backend/spacedata.go @@ -0,0 +1,44 @@ +package main + +type Feed struct { + Type string `json:"type"` + Url string `json:"url"` +} + +type Location struct { + Address string `json:"address"` + Lon float32 `json:"lon"` + Lat float32 `json:"lat"` +} + +type Contact struct { + Twitter string `json:"twitter"` + Phone string `json:"phone"` + Irc string `json:"irc"` + Email string `json:"email"` + Ml string `json:"ml"` + IssueMail string `json:"issue_mail"` +} + +type State struct { + Open bool `json:"open"` +} + +type Feeds struct { + Blog Feed `json:"blog"` + Wiki Feed `json:"wiki"` + Calendar Feed `json:"calendar"` +} + +type SpaceData struct { + Api string `json:"api"` + Space string `json:"space"` + Logo string `json:"logo"` + Url string `json:"url"` + Location Location `json:"location"` + Contact Contact `json:"contact"` + IssueReportChannels []string `json:"issue_report_channels"` + State State `json:"state"` + Feeds Feeds `json:"feeds"` + Ext_ccc string `json:"ext_ccc"` +} \ No newline at end of file diff --git a/backend/spaceurl.go b/backend/spaceurl.go new file mode 100644 index 0000000..37e160f --- /dev/null +++ b/backend/spaceurl.go @@ -0,0 +1,7 @@ +package main + +type SpaceUrl struct { + Url string `json:"url"` + Validated bool `json:"validated"` + LastUpdated int64 `json:"lastUpdated"` +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a0b788a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,17 @@ +version: "3" +services: + frontend: + build: ./frontend + image: gidsi/spaceapi-ccc-frontend:latest + depends_on: + - backend + backend: + build: ./backend + image: gidsi/spaceapi-ccc-backend:latest + env_file: .env + depends_on: + - database + database: + image: mongo:latest + volumes: + - /opt/eva:/data/db diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..927d17b --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,18 @@ +# See https://help.github.com/ignore-files/ for more about ignoring files. + +# dependencies +/node_modules + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env +npm-debug.log* +yarn-debug.log* +yarn-error.log* + diff --git a/frontend/.travis.yml b/frontend/.travis.yml new file mode 100644 index 0000000..9bfdf5a --- /dev/null +++ b/frontend/.travis.yml @@ -0,0 +1,11 @@ +language: node_js + +node_js: + - "5" + +cache: + directories: + - node_modules + +script: + - npm run deploy diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..54b316a --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,11 @@ +FROM node as builder +WORKDIR /app +COPY . . + +RUN npm install +RUN npm run build + +FROM nginx:alpine +COPY --from=builder /app/build/ /usr/share/nginx/html/ +RUN mkdir -p /tmp/osm/cache +COPY nginx.conf /etc/nginx/nginx.conf diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..2285286 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,11 @@ +eve-frontend +======================= + +Usage +----- +``` +npm install -g yarn +yarn install +yarn start + +``` \ No newline at end of file diff --git a/frontend/nginx.conf b/frontend/nginx.conf new file mode 100644 index 0000000..7884af6 --- /dev/null +++ b/frontend/nginx.conf @@ -0,0 +1,59 @@ +worker_processes 1; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + + keepalive_timeout 65; + + gzip on; + + proxy_cache_path /tmp/osm/cache keys_zone=osm:512m inactive=7d; + proxy_temp_path /tmp/osm/temp; + upstream osm { + server b.basemaps.cartocdn.com; + #server b.tile.openstreetmap.org; + keepalive 8; + } + + + server { + listen 80; + server_name localhost; + + location / { + root /usr/share/nginx/html; + try_files $uri /index.html; + } + + location /static { + root /usr/share/nginx/html/; + } + + location /api/ { + proxy_pass http://backend:8080/; + proxy_http_version 1.1; + proxy_set_header X-Forwarded-Proto https; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + } + + location /map/tiles/ { + proxy_set_header Host b.basemaps.cartocdn.com; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_pass http://osm/dark_all/; + proxy_cache osm; + proxy_cache_valid 7d; + expires 7d; + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..9bfc04b --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,30 @@ +{ + "name": "eva-frontend", + "version": "1.0.0", + "private": true, + "dependencies": { + "leaflet": "^1.0.3", + "material-ui": "^0.17.4", + "moment": "^2.18.1", + "react": "^15.5.4", + "react-dom": "^15.5.4", + "react-leaflet": "^1.1.6", + "react-redux": "^5.0.4", + "react-router-dom": "^4.1.1", + "react-tap-event-plugin": "^2.0.1", + "redux": "^3.6.0", + "redux-actions": "^2.0.2", + "redux-thunk": "^2.2.0", + "rrule": "^2.2.0", + "superagent": "^3.5.2" + }, + "devDependencies": { + "react-scripts": "0.9.5" + }, + "scripts": { + "start": "react-scripts start", + "build": "react-scripts build", + "test": "react-scripts test --env=jsdom", + "eject": "react-scripts eject" + } +} diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..cd6afd2 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..e3c910e --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,21 @@ + + + + + + + + + CCC Spaces + + +
+ +
+ + diff --git a/frontend/public/leaflet.css b/frontend/public/leaflet.css new file mode 100644 index 0000000..3598308 --- /dev/null +++ b/frontend/public/leaflet.css @@ -0,0 +1,618 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-map-pane svg, +.leaflet-map-pane canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg, +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer { + max-width: none !important; + } + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + } +.leaflet-container.leaflet-touch-drag.leaflet-touch-drag { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.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: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + 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 { + will-change: opacity; + } +.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-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +.leaflet-zoom-anim .leaflet-zoom-animated { + will-change: transform; + } +.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 { + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* 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-scrollbar { + overflow-y: scroll; + padding-right: 5px; + } +.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: border-box; + box-sizing: border-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; + margin-bottom: 20px; + } +.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 { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.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; + color: #333; + 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; + border: none; + 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; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-clickable { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } diff --git a/frontend/public/leaflet.js b/frontend/public/leaflet.js new file mode 100644 index 0000000..8e0f4af --- /dev/null +++ b/frontend/public/leaflet.js @@ -0,0 +1,9 @@ +/* + Leaflet 1.0.0-rc.3, a JS library for interactive maps. http://leafletjs.com + (c) 2010-2015 Vladimir Agafonkin, (c) 2010-2011 CloudMade +*/ +!function(t,e,i){function n(){var e=t.L;o.noConflict=function(){return t.L=e,this},t.L=o}var o={version:"1.0.0-rc.3"};"object"==typeof module&&"object"==typeof module.exports?module.exports=o:"function"==typeof define&&define.amd&&define(o),"undefined"!=typeof t&&n(),o.Util={extend:function(t){var e,i,n,o;for(i=1,n=arguments.length;i1}}(),o.Point=function(t,e,i){this.x=i?Math.round(t):t,this.y=i?Math.round(e):e},o.Point.prototype={clone:function(){return new o.Point(this.x,this.y)},add:function(t){return this.clone()._add(o.point(t))},_add:function(t){return this.x+=t.x,this.y+=t.y,this},subtract:function(t){return this.clone()._subtract(o.point(t))},_subtract:function(t){return this.x-=t.x,this.y-=t.y,this},divideBy:function(t){return this.clone()._divideBy(t)},_divideBy:function(t){return this.x/=t,this.y/=t,this},multiplyBy:function(t){return this.clone()._multiplyBy(t)},_multiplyBy:function(t){return this.x*=t,this.y*=t,this},scaleBy:function(t){return new o.Point(this.x*t.x,this.y*t.y)},unscaleBy:function(t){return new o.Point(this.x/t.x,this.y/t.y)},round:function(){return this.clone()._round()},_round:function(){return this.x=Math.round(this.x),this.y=Math.round(this.y),this},floor:function(){return this.clone()._floor()},_floor:function(){return this.x=Math.floor(this.x),this.y=Math.floor(this.y),this},ceil:function(){return this.clone()._ceil()},_ceil:function(){return this.x=Math.ceil(this.x),this.y=Math.ceil(this.y),this},distanceTo:function(t){t=o.point(t);var e=t.x-this.x,i=t.y-this.y;return Math.sqrt(e*e+i*i)},equals:function(t){return t=o.point(t),t.x===this.x&&t.y===this.y},contains:function(t){return t=o.point(t),Math.abs(t.x)<=Math.abs(this.x)&&Math.abs(t.y)<=Math.abs(this.y)},toString:function(){return"Point("+o.Util.formatNum(this.x)+", "+o.Util.formatNum(this.y)+")"}},o.point=function(t,e,n){return t instanceof o.Point?t:o.Util.isArray(t)?new o.Point(t[0],t[1]):t===i||null===t?t:"object"==typeof t&&"x"in t&&"y"in t?new o.Point(t.x,t.y):new o.Point(t,e,n)},o.Bounds=function(t,e){if(t)for(var i=e?[t,e]:t,n=0,o=i.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=o.bounds(t);var e=this.min,i=this.max,n=t.min,s=t.max,r=s.x>=e.x&&n.x<=i.x,a=s.y>=e.y&&n.y<=i.y;return r&&a},overlaps:function(t){t=o.bounds(t);var e=this.min,i=this.max,n=t.min,s=t.max,r=s.x>e.x&&n.xe.y&&n.y0&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(n)},addClass:function(t,e){if(t.classList!==i)for(var n=o.Util.splitWords(e),s=0,r=n.length;s=n.lat&&i.lat<=s.lat&&e.lng>=n.lng&&i.lng<=s.lng},intersects:function(t){t=o.latLngBounds(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),s=t.getNorthEast(),r=s.lat>=e.lat&&n.lat<=i.lat,a=s.lng>=e.lng&&n.lng<=i.lng;return r&&a},overlaps:function(t){t=o.latLngBounds(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),s=t.getNorthEast(),r=s.lat>e.lat&&n.late.lng&&n.lngthis.options.maxZoom?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),n=this._limitCenter(i,this._zoom,o.latLngBounds(t));return i.equals(n)||this.panTo(n,e),this._enforcingBounds=!1,this},invalidateSize:function(t){if(!this._loaded)return this;t=o.extend({animate:!1,pan:!0},t===!0?{animate:!0}:t);var e=this.getSize();this._sizeChanged=!0,this._lastCenter=null;var i=this.getSize(),n=e.divideBy(2).round(),s=i.divideBy(2).round(),r=n.subtract(s);return r.x||r.y?(t.animate&&t.pan?this.panBy(r):(t.pan&&this._rawPanBy(r),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(o.bind(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},addHandler:function(t,e){if(!e)return this;var i=this[t]=new e(this);return this._handlers.push(i),this.options[t]&&i.enable(),this},remove:function(){this._initEvents(!0);try{delete this._container._leaflet}catch(t){this._container._leaflet=i}o.DomUtil.remove(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._clearHandlers(),this._loaded&&this.fire("unload");for(var e in this._layers)this._layers[e].remove();return this},createPane:function(t,e){var i="leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),n=o.DomUtil.create("div",i,e||this._mapPane);return t&&(this._panes[t]=n),n},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter:this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds(),e=this.unproject(t.getBottomLeft()),i=this.unproject(t.getTopRight());return new o.LatLngBounds(e,i)},getMinZoom:function(){return this.options.minZoom===i?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return this.options.maxZoom===i?this._layersMaxZoom===i?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=o.latLngBounds(t),i=o.point(i||[0,0]);var n=this.getZoom()||0,s=this.getMinZoom(),r=this.getMaxZoom(),a=t.getNorthWest(),h=t.getSouthEast(),l=this.getSize().subtract(i),u=this.project(h,n).subtract(this.project(a,n)),c=o.Browser.any3d?this.options.zoomSnap:1,d=Math.min(l.x/u.x,l.y/u.y);return n=this.getScaleZoom(d,n),c&&(n=Math.round(n/(c/100))*(c/100),n=e?Math.ceil(n/c)*c:Math.floor(n/c)*c),Math.max(s,Math.min(r,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new o.Point(this._container.clientWidth,this._container.clientHeight),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){var i=this._getTopLeftPoint(t,e);return new o.Bounds(i,i.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(t===i?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var n=this.options.crs;return e=e===i?this._zoom:e,n.scale(t)/n.scale(e)},getScaleZoom:function(t,e){var n=this.options.crs;return e=e===i?this._zoom:e,n.zoom(t*n.scale(e))},project:function(t,e){return e=e===i?this._zoom:e,this.options.crs.latLngToPoint(o.latLng(t),e)},unproject:function(t,e){return e=e===i?this._zoom:e,this.options.crs.pointToLatLng(o.point(t),e)},layerPointToLatLng:function(t){var e=o.point(t).add(this.getPixelOrigin());return this.unproject(e)},latLngToLayerPoint:function(t){var e=this.project(o.latLng(t))._round();return e._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(o.latLng(t))},distance:function(t,e){return this.options.crs.distance(o.latLng(t),o.latLng(e))},containerPointToLayerPoint:function(t){return o.point(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return o.point(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){var e=this.containerPointToLayerPoint(o.point(t));return this.layerPointToLatLng(e)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(o.latLng(t)))},mouseEventToContainerPoint:function(t){return o.DomEvent.getMousePosition(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){var e=this._container=o.DomUtil.get(t);if(!e)throw new Error("Map container not found.");if(e._leaflet)throw new Error("Map container is already initialized.");o.DomEvent.addListener(e,"scroll",this._onScroll,this),e._leaflet=!0},_initLayout:function(){var t=this._container;this._fadeAnimated=this.options.fadeAnimation&&o.Browser.any3d,o.DomUtil.addClass(t,"leaflet-container"+(o.Browser.touch?" leaflet-touch":"")+(o.Browser.retina?" leaflet-retina":"")+(o.Browser.ielt9?" leaflet-oldie":"")+(o.Browser.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":""));var e=o.DomUtil.getStyle(t,"position");"absolute"!==e&&"relative"!==e&&"fixed"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),o.DomUtil.setPosition(this._mapPane,new o.Point(0,0)),this.createPane("tilePane"),this.createPane("shadowPane"),this.createPane("overlayPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(o.DomUtil.addClass(t.markerPane,"leaflet-zoom-hide"),o.DomUtil.addClass(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e){o.DomUtil.setPosition(this._mapPane,new o.Point(0,0));var i=!this._loaded;this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset");var n=this._zoom!==e;this._moveStart(n)._move(t,e)._moveEnd(n),this.fire("viewreset"),i&&this.fire("load")},_moveStart:function(t){return t&&this.fire("zoomstart"),this.fire("movestart")},_move:function(t,e,n){e===i&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),(o||n&&n.pinch)&&this.fire("zoom",n),this.fire("move",n)},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return o.Util.cancelAnimFrame(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){o.DomUtil.setPosition(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(e){if(o.DomEvent){this._targets={},this._targets[o.stamp(this._container)]=this;var i=e?"off":"on";o.DomEvent[i](this._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress",this._handleDOMEvent,this),this.options.trackResize&&o.DomEvent[i](t,"resize",this._onResize,this),o.Browser.any3d&&this.options.transform3DLimit&&this[i]("moveend",this._onMoveEnd)}},_onResize:function(){o.Util.cancelAnimFrame(this._resizeRequest),this._resizeRequest=o.Util.requestAnimFrame(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],s="mouseout"===e||"mouseover"===e,r=t.target||t.srcElement,a=!1;r;){if(i=this._targets[o.stamp(r)],i&&("click"===e||"preclick"===e)&&!t._simulated&&this._draggableMoved(i)){a=!0;break}if(i&&i.listens(e,!0)){if(s&&!o.DomEvent._isExternalTarget(r,t))break;if(n.push(i),s)break}if(r===this._container)break;r=r.parentNode}return n.length||a||s||!o.DomEvent._isExternalTarget(r,t)||(n=[this]),n},_handleDOMEvent:function(t){if(this._loaded&&!o.DomEvent._skipped(t)){var e="keypress"===t.type&&13===t.keyCode?"click":t.type;"mousedown"===e&&o.DomUtil.preventOutline(t.target||t.srcElement),this._fireDOMEvent(t,e)}},_fireDOMEvent:function(t,e,i){if("click"===t.type){var n=o.Util.extend({},t);n.type="preclick",this._fireDOMEvent(n,n.type,i)}if(!t._stopped&&(i=(i||[]).concat(this._findEventTargets(t,e)),i.length)){var s=i[0];"contextmenu"===e&&s.listens(e,!0)&&o.DomEvent.preventDefault(t);var r={originalEvent:t};if("keypress"!==t.type){var a=s instanceof o.Marker;r.containerPoint=a?this.latLngToContainerPoint(s.getLatLng()):this.mouseEventToContainerPoint(t),r.layerPoint=this.containerPointToLayerPoint(r.containerPoint),r.latlng=a?s.getLatLng():this.layerPointToLatLng(r.layerPoint)}for(var h=0;h0?Math.round(t-e)/2:Math.max(0,Math.ceil(t))-Math.max(0,Math.floor(e))},_limitZoom:function(t){var e=this.getMinZoom(),i=this.getMaxZoom(),n=o.Browser.any3d?this.options.zoomSnap:1;return n&&(t=Math.round(t/n)*n),Math.max(e,Math.min(i,t))}}),o.map=function(t,e){return new o.Map(t,e)},o.Layer=o.Evented.extend({options:{pane:"overlayPane",nonBubblingEvents:[]},addTo:function(t){return t.addLayer(this),this},remove:function(){return this.removeFrom(this._map||this._mapToAdd)},removeFrom:function(t){return t&&t.removeLayer(this),this},getPane:function(t){return this._map.getPane(t?this.options[t]||t:this.options.pane)},addInteractiveTarget:function(t){return this._map._targets[o.stamp(t)]=this,this},removeInteractiveTarget:function(t){return delete this._map._targets[o.stamp(t)],this},_layerAdd:function(t){var e=t.target;if(e.hasLayer(this)){if(this._map=e,this._zoomAnimated=e._zoomAnimated,this.getEvents){var i=this.getEvents();e.on(i,this),this.once("remove",function(){e.off(i,this)},this)}this.onAdd(e),this.getAttribution&&this._map.attributionControl&&this._map.attributionControl.addAttribution(this.getAttribution()),this.fire("add"),e.fire("layeradd",{layer:this})}}}),o.Map.include({addLayer:function(t){var e=o.stamp(t);return this._layers[e]?this:(this._layers[e]=t,t._mapToAdd=this,t.beforeAdd&&t.beforeAdd(this),this.whenReady(t._layerAdd,t),this)},removeLayer:function(t){var e=o.stamp(t);return this._layers[e]?(this._loaded&&t.onRemove(this),t.getAttribution&&this.attributionControl&&this.attributionControl.removeAttribution(t.getAttribution()),delete this._layers[e],this._loaded&&(this.fire("layerremove",{layer:t}),t.fire("remove")),t._map=t._mapToAdd=null,this):this},hasLayer:function(t){return!!t&&o.stamp(t)in this._layers},eachLayer:function(t,e){for(var i in this._layers)t.call(e,this._layers[i]);return this},_addLayers:function(t){t=t?o.Util.isArray(t)?t:[t]:[];for(var e=0,i=t.length;e1e-7;l++)e=r*Math.sin(h),e=Math.pow((1-e)/(1+e),r/2),u=Math.PI/2-2*Math.atan(a*e)-h,h+=u;return new o.LatLng(h*i,t.x*i/n)}},o.CRS.EPSG3395=o.extend({},o.CRS.Earth,{code:"EPSG:3395",projection:o.Projection.Mercator,transformation:function(){var t=.5/(Math.PI*o.Projection.Mercator.R);return new o.Transformation(t,.5,(-t),.5)}()}),o.GridLayer=o.Layer.extend({options:{tileSize:256,opacity:1,updateWhenIdle:o.Browser.mobile,updateWhenZooming:!0,updateInterval:200,attribution:null,zIndex:1,bounds:null,minZoom:0,maxZoom:i,noWrap:!1,pane:"tilePane",className:"",keepBuffer:2},initialize:function(t){t=o.setOptions(this,t)},onAdd:function(){this._initContainer(),this._levels={},this._tiles={},this._resetView(),this._update()},beforeAdd:function(t){t._addZoomLimit(this)},onRemove:function(t){this._removeAllTiles(),o.DomUtil.remove(this._container),t._removeZoomLimit(this),this._container=null,this._tileZoom=null},bringToFront:function(){return this._map&&(o.DomUtil.toFront(this._container),this._setAutoZIndex(Math.max)),this},bringToBack:function(){return this._map&&(o.DomUtil.toBack(this._container),this._setAutoZIndex(Math.min)),this},getAttribution:function(){return this.options.attribution},getContainer:function(){return this._container},setOpacity:function(t){return this.options.opacity=t,this._updateOpacity(),this},setZIndex:function(t){return this.options.zIndex=t,this._updateZIndex(),this},isLoading:function(){return this._loading},redraw:function(){return this._map&&(this._removeAllTiles(),this._update()),this},getEvents:function(){var t={viewprereset:this._invalidateAll,viewreset:this._resetView,zoom:this._resetView,moveend:this._onMoveEnd};return this.options.updateWhenIdle||(this._onMove||(this._onMove=o.Util.throttle(this._onMoveEnd,this.options.updateInterval,this)),t.move=this._onMove),this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},createTile:function(){return e.createElement("div")},getTileSize:function(){var t=this.options.tileSize;return t instanceof o.Point?t:new o.Point(t,t)},_updateZIndex:function(){this._container&&this.options.zIndex!==i&&null!==this.options.zIndex&&(this._container.style.zIndex=this.options.zIndex)},_setAutoZIndex:function(t){for(var e,i=this.getPane().children,n=-t(-(1/0),1/0),o=0,s=i.length;othis.options.maxZoom||in&&this._retainParent(s,r,a,n))},_retainChildren:function(t,e,i,n){for(var s=2*t;s<2*t+2;s++)for(var r=2*e;r<2*e+2;r++){var a=new o.Point(s,r);a.z=i+1;var h=this._tileCoordsToKey(a),l=this._tiles[h];l&&l.active?l.retain=!0:(l&&l.loaded&&(l.retain=!0),i+1this.options.maxZoom||this.options.minZoom!==i&&s1)return void this._setView(t,s);for(var m=a.min.y;m<=a.max.y;m++)for(var p=a.min.x;p<=a.max.x;p++){var f=new o.Point(p,m);if(f.z=this._tileZoom,this._isValidTile(f)){var g=this._tiles[this._tileCoordsToKey(f)];g?g.current=!0:l.push(f)}}if(l.sort(function(t,e){return t.distanceTo(h)-e.distanceTo(h)}),0!==l.length){this._loading||(this._loading=!0,this.fire("loading"));var v=e.createDocumentFragment();for(p=0;pi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}if(!this.options.bounds)return!0;var n=this._tileCoordsToBounds(t);return o.latLngBounds(this.options.bounds).overlaps(n)},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToBounds:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),s=n.add(i),r=e.wrapLatLng(e.unproject(n,t.z)),a=e.wrapLatLng(e.unproject(s,t.z));return new o.LatLngBounds(r,a)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var e=t.split(":"),i=new o.Point((+e[0]),(+e[1]));return i.z=+e[2],i},_removeTile:function(t){var e=this._tiles[t];e&&(o.DomUtil.remove(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){o.DomUtil.addClass(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=o.Util.falseFn,t.onmousemove=o.Util.falseFn,o.Browser.ielt9&&this.options.opacity<1&&o.DomUtil.setOpacity(t,this.options.opacity),o.Browser.android&&!o.Browser.android23&&(t.style.WebkitBackfaceVisibility="hidden")},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),s=this.createTile(this._wrapCoords(t),o.bind(this._tileReady,this,t));this._initTile(s),this.createTile.length<2&&o.Util.requestAnimFrame(o.bind(this._tileReady,this,t,null,s)),o.DomUtil.setPosition(s,i),this._tiles[n]={el:s,coords:t,current:!0},e.appendChild(s),this.fire("tileloadstart",{tile:s,coords:t})},_tileReady:function(t,e,i){if(this._map){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);i=this._tiles[n],i&&(i.loaded=+new Date,this._map._fadeAnimated?(o.DomUtil.setOpacity(i.el,0),o.Util.cancelAnimFrame(this._fadeFrame),this._fadeFrame=o.Util.requestAnimFrame(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),o.DomUtil.addClass(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t}),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),o.Browser.ielt9||!this._map._fadeAnimated?o.Util.requestAnimFrame(this._pruneTiles,this):setTimeout(o.bind(this._pruneTiles,this),250)))}},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new o.Point(this._wrapX?o.Util.wrapNum(t.x,this._wrapX):t.x,this._wrapY?o.Util.wrapNum(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new o.Bounds(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}}),o.gridLayer=function(t){return new o.GridLayer(t)},o.TileLayer=o.GridLayer.extend({options:{minZoom:0,maxZoom:18,maxNativeZoom:null,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1},initialize:function(t,e){this._url=t,e=o.setOptions(this,e),e.detectRetina&&o.Browser.retina&&e.maxZoom>0&&(e.tileSize=Math.floor(e.tileSize/2),e.zoomReverse?(e.zoomOffset--,e.minZoom++):(e.zoomOffset++,e.maxZoom--),e.minZoom=Math.max(0,e.minZoom)),"string"==typeof e.subdomains&&(e.subdomains=e.subdomains.split("")),o.Browser.android||this.on("tileunload",this._onTileRemove)},setUrl:function(t,e){return this._url=t,e||this.redraw(),this},createTile:function(t,i){var n=e.createElement("img");return o.DomEvent.on(n,"load",o.bind(this._tileOnLoad,this,i,n)),o.DomEvent.on(n,"error",o.bind(this._tileOnError,this,i,n)),this.options.crossOrigin&&(n.crossOrigin=""),n.alt="",n.src=this.getTileUrl(t),n},getTileUrl:function(t){var e={r:o.Browser.retina?"@2x":"",s:this._getSubdomain(t),x:t.x,y:t.y,z:this._getZoomForUrl()};if(this._map&&!this._map.options.crs.infinite){var i=this._globalTileRange.max.y-t.y;this.options.tms&&(e.y=i),e["-y"]=i}return o.Util.template(this._url,o.extend(e,this.options))},_tileOnLoad:function(t,e){o.Browser.ielt9?setTimeout(o.bind(t,this,null,e),0):t(null,e)},_tileOnError:function(t,e,i){var n=this.options.errorTileUrl;n&&(e.src=n),t(i,e)},getTileSize:function(){var t=this._map,e=o.GridLayer.prototype.getTileSize.call(this),i=this._tileZoom+this.options.zoomOffset,n=this.options.maxNativeZoom;return null!==n&&i>n?e.divideBy(t.getZoomScale(n,i)).round():e},_onTileRemove:function(t){t.tile.onload=null},_getZoomForUrl:function(){var t=this.options,e=this._tileZoom;return t.zoomReverse&&(e=t.maxZoom-e),e+=t.zoomOffset,null!==t.maxNativeZoom?Math.min(e,t.maxNativeZoom):e},_getSubdomain:function(t){var e=Math.abs(t.x+t.y)%this.options.subdomains.length;return this.options.subdomains[e]},_abortLoading:function(){var t,e;for(t in this._tiles)this._tiles[t].coords.z!==this._tileZoom&&(e=this._tiles[t].el,e.onload=o.Util.falseFn,e.onerror=o.Util.falseFn,e.complete||(e.src=o.Util.emptyImageUrl,o.DomUtil.remove(e)))}}),o.tileLayer=function(t,e){return new o.TileLayer(t,e)},o.TileLayer.WMS=o.TileLayer.extend({defaultWmsParams:{service:"WMS",request:"GetMap",layers:"",styles:"",format:"image/jpeg",transparent:!1,version:"1.1.1"},options:{crs:null,uppercase:!1},initialize:function(t,e){this._url=t;var i=o.extend({},this.defaultWmsParams);for(var n in e)n in this.options||(i[n]=e[n]);e=o.setOptions(this,e),i.width=i.height=e.tileSize*(e.detectRetina&&o.Browser.retina?2:1),this.wmsParams=i},onAdd:function(t){this._crs=this.options.crs||t.options.crs,this._wmsVersion=parseFloat(this.wmsParams.version);var e=this._wmsVersion>=1.3?"crs":"srs";this.wmsParams[e]=this._crs.code,o.TileLayer.prototype.onAdd.call(this,t)},getTileUrl:function(t){var e=this._tileCoordsToBounds(t),i=this._crs.project(e.getNorthWest()),n=this._crs.project(e.getSouthEast()),s=(this._wmsVersion>=1.3&&this._crs===o.CRS.EPSG4326?[n.y,i.x,i.y,n.x]:[i.x,n.y,n.x,i.y]).join(","),r=o.TileLayer.prototype.getTileUrl.call(this,t);return r+o.Util.getParamString(this.wmsParams,r,this.options.uppercase)+(this.options.uppercase?"&BBOX=":"&bbox=")+s},setParams:function(t,e){return o.extend(this.wmsParams,t),e||this.redraw(),this}}),o.tileLayer.wms=function(t,e){return new o.TileLayer.WMS(t,e)},o.ImageOverlay=o.Layer.extend({options:{opacity:1,alt:"",interactive:!1,attribution:null,crossOrigin:!1},initialize:function(t,e,i){this._url=t,this._bounds=o.latLngBounds(e),o.setOptions(this,i)},onAdd:function(){this._image||(this._initImage(),this.options.opacity<1&&this._updateOpacity()),this.options.interactive&&(o.DomUtil.addClass(this._image,"leaflet-interactive"),this.addInteractiveTarget(this._image)),this.getPane().appendChild(this._image),this._reset()},onRemove:function(){o.DomUtil.remove(this._image),this.options.interactive&&this.removeInteractiveTarget(this._image)},setOpacity:function(t){return this.options.opacity=t,this._image&&this._updateOpacity(),this},setStyle:function(t){return t.opacity&&this.setOpacity(t.opacity),this},bringToFront:function(){return this._map&&o.DomUtil.toFront(this._image),this},bringToBack:function(){return this._map&&o.DomUtil.toBack(this._image),this},setUrl:function(t){return this._url=t,this._image&&(this._image.src=t),this},setBounds:function(t){return this._bounds=t,this._map&&this._reset(),this},getAttribution:function(){return this.options.attribution},getEvents:function(){var t={zoom:this._reset,viewreset:this._reset};return this._zoomAnimated&&(t.zoomanim=this._animateZoom),t},getBounds:function(){return this._bounds},getElement:function(){return this._image},_initImage:function(){var t=this._image=o.DomUtil.create("img","leaflet-image-layer "+(this._zoomAnimated?"leaflet-zoom-animated":""));t.onselectstart=o.Util.falseFn,t.onmousemove=o.Util.falseFn,t.onload=o.bind(this.fire,this,"load"),this.options.crossOrigin&&(t.crossOrigin=""),t.src=this._url,t.alt=this.options.alt},_animateZoom:function(t){var e=this._map.getZoomScale(t.zoom),i=this._map._latLngToNewLayerPoint(this._bounds.getNorthWest(),t.zoom,t.center);o.DomUtil.setTransform(this._image,i,e)},_reset:function(){var t=this._image,e=new o.Bounds(this._map.latLngToLayerPoint(this._bounds.getNorthWest()),this._map.latLngToLayerPoint(this._bounds.getSouthEast())),i=e.getSize();o.DomUtil.setPosition(t,e.min),t.style.width=i.x+"px",t.style.height=i.y+"px"},_updateOpacity:function(){o.DomUtil.setOpacity(this._image,this.options.opacity)}}),o.imageOverlay=function(t,e,i){return new o.ImageOverlay(t,e,i)},o.Icon=o.Class.extend({initialize:function(t){o.setOptions(this,t)},createIcon:function(t){return this._createIcon("icon",t)},createShadow:function(t){return this._createIcon("shadow",t)},_createIcon:function(t,e){var i=this._getIconUrl(t);if(!i){if("icon"===t)throw new Error("iconUrl not set in Icon options (see the docs).");return null}var n=this._createImg(i,e&&"IMG"===e.tagName?e:null);return this._setIconStyles(n,t),n},_setIconStyles:function(t,e){var i=this.options,n=i[e+"Size"];"number"==typeof n&&(n=[n,n]);var s=o.point(n),r=o.point("shadow"===e&&i.shadowAnchor||i.iconAnchor||s&&s.divideBy(2,!0));t.className="leaflet-marker-"+e+" "+(i.className||""),r&&(t.style.marginLeft=-r.x+"px",t.style.marginTop=-r.y+"px"),s&&(t.style.width=s.x+"px",t.style.height=s.y+"px")},_createImg:function(t,i){return i=i||e.createElement("img"),i.src=t,i},_getIconUrl:function(t){return o.Browser.retina&&this.options[t+"RetinaUrl"]||this.options[t+"Url"]}}),o.icon=function(t){return new o.Icon(t)},o.Icon.Default=o.Icon.extend({options:{iconSize:[25,41],iconAnchor:[12,41],popupAnchor:[1,-34],tooltipAnchor:[16,-28],shadowSize:[41,41]},_getIconUrl:function(t){var e=t+"Url";if(this.options[e])return this.options[e];var i=o.Icon.Default.imagePath;if(!i)throw new Error("Couldn't autodetect L.Icon.Default.imagePath, set it manually.");return i+"/marker-"+t+(o.Browser.retina&&"icon"===t?"-2x":"")+".png"}}),o.Icon.Default.imagePath=function(){var t,i,n,o,s=e.getElementsByTagName("script"),r=/[\/^]leaflet[\-\._]?([\w\-\._]*)\.js\??/;for(t=0,i=s.length;ts?(e.height=s+"px",o.DomUtil.addClass(t,r)):o.DomUtil.removeClass(t,r),this._containerWidth=this._container.offsetWidth},_animateZoom:function(t){var e=this._map._latLngToNewLayerPoint(this._latlng,t.zoom,t.center),i=this._getAnchor();o.DomUtil.setPosition(this._container,e.add(i))},_adjustPan:function(){if(!(!this.options.autoPan||this._map._panAnim&&this._map._panAnim._inProgress)){var t=this._map,e=parseInt(o.DomUtil.getStyle(this._container,"marginBottom"),10)||0,i=this._container.offsetHeight+e,n=this._containerWidth,s=new o.Point(this._containerLeft,-i-this._containerBottom);this._zoomAnimated&&s._add(o.DomUtil.getPosition(this._container));var r=t.layerPointToContainerPoint(s),a=o.point(this.options.autoPanPadding),h=o.point(this.options.autoPanPaddingTopLeft||a),l=o.point(this.options.autoPanPaddingBottomRight||a),u=t.getSize(),c=0,d=0;r.x+n+l.x>u.x&&(c=r.x+n-u.x+l.x),r.x-c-h.x<0&&(c=r.x-h.x),r.y+i+l.y>u.y&&(d=r.y+i-u.y+l.y), +r.y-d-h.y<0&&(d=r.y-h.y),(c||d)&&t.fire("autopanstart").panBy([c,d])}},_onCloseButtonClick:function(t){this._close(),o.DomEvent.stop(t)},_getAnchor:function(){return o.point(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}}),o.popup=function(t,e){return new o.Popup(t,e)},o.Map.mergeOptions({closePopupOnClick:!0}),o.Map.include({openPopup:function(t,e,i){return t instanceof o.Popup||(t=new o.Popup(i).setContent(t)),e&&t.setLatLng(e),this.hasLayer(t)?this:(this._popup&&this._popup.options.autoClose&&this.closePopup(),this._popup=t,this.addLayer(t))},closePopup:function(t){return t&&t!==this._popup||(t=this._popup,this._popup=null),t&&this.removeLayer(t),this}}),o.Layer.include({bindPopup:function(t,e){return t instanceof o.Popup?(o.setOptions(t,e),this._popup=t,t._source=this):(this._popup&&!e||(this._popup=new o.Popup(e,this)),this._popup.setContent(t)),this._popupHandlersAdded||(this.on({click:this._openPopup,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t,e){if(t instanceof o.Layer||(e=t,t=this),t instanceof o.FeatureGroup)for(var i in this._layers){t=this._layers[i];break}return e||(e=t.getCenter?t.getCenter():t.getLatLng()),this._popup&&this._map&&(this._popup._source=t,this._popup.update(),this._map.openPopup(this._popup,e)),this},closePopup:function(){return this._popup&&this._popup._close(),this},togglePopup:function(t){return this._popup&&(this._popup._map?this.closePopup():this.openPopup(t)),this},isPopupOpen:function(){return this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e=t.layer||t.target;if(this._popup&&this._map)return o.DomEvent.stop(t),e instanceof o.Path?void this.openPopup(t.layer||t.target,t.latlng):void(this._map.hasLayer(this._popup)&&this._popup._source===e?this.closePopup():this.openPopup(e,t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)}}),o.Marker.include({_getPopupAnchor:function(){return this.options.icon.options.popupAnchor||[0,0]}}),o.Tooltip=o.DivOverlay.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,interactive:!1,opacity:.9},onAdd:function(t){o.DivOverlay.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&this._source.fire("tooltipopen",{tooltip:this},!0)},onRemove:function(t){o.DivOverlay.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&this._source.fire("tooltipclose",{tooltip:this},!0)},getEvents:function(){var t=o.DivOverlay.prototype.getEvents.call(this);return o.Browser.touch&&!this.options.permanent&&(t.preclick=this._close),t},_close:function(){this._map&&this._map.closeTooltip(this)},_initLayout:function(){var t="leaflet-tooltip",e=t+" "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=o.DomUtil.create("div",e)},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e=this._map,i=this._container,n=e.latLngToContainerPoint(e.getCenter()),s=e.layerPointToContainerPoint(t),r=this.options.direction,a=i.offsetWidth,h=i.offsetHeight,l=o.point(this.options.offset),u=this._getAnchor();"top"===r?t=t.add(o.point(-a/2+l.x,-h+l.y+u.y)):"bottom"===r?t=t.subtract(o.point(a/2-l.x,-l.y)):"center"===r?t=t.subtract(o.point(a/2+l.x,h/2-u.y+l.y)):"right"===r||"auto"===r&&s.xh&&(s=r,h=a);h>i&&(e[s]=1,this._simplifyDPStep(t,e,i,n,s),this._simplifyDPStep(t,e,i,s,o))},_reducePoints:function(t,e){for(var i=[t[0]],n=1,o=0,s=t.length;ne&&(i.push(t[n]),o=n);return oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i},_sqDist:function(t,e){var i=e.x-t.x,n=e.y-t.y;return i*i+n*n},_sqClosestPointOnSegment:function(t,e,i,n){var s,r=e.x,a=e.y,h=i.x-r,l=i.y-a,u=h*h+l*l;return u>0&&(s=((t.x-r)*h+(t.y-a)*l)/u,s>1?(r=i.x,a=i.y):s>0&&(r+=h*s,a+=l*s)),h=t.x-r,l=t.y-a,n?h*h+l*l:new o.Point(r,a)}},o.Polyline=o.Path.extend({options:{smoothFactor:1,noClip:!1},initialize:function(t,e){o.setOptions(this,e),this._setLatLngs(t)},getLatLngs:function(){return this._latlngs},setLatLngs:function(t){return this._setLatLngs(t),this.redraw()},isEmpty:function(){return!this._latlngs.length},closestLayerPoint:function(t){for(var e,i,n=1/0,s=null,r=o.LineUtil._sqClosestPointOnSegment,a=0,h=this._parts.length;ae)return r=(n-e)/i,this._map.layerPointToLatLng([s.x-r*(s.x-o.x),s.y-r*(s.y-o.y)])},getBounds:function(){return this._bounds},addLatLng:function(t,e){return e=e||this._defaultShape(),t=o.latLng(t),e.push(t),this._bounds.extend(t),this.redraw()},_setLatLngs:function(t){this._bounds=new o.LatLngBounds,this._latlngs=this._convertLatLngs(t)},_defaultShape:function(){return o.Polyline._flat(this._latlngs)?this._latlngs:this._latlngs[0]},_convertLatLngs:function(t){for(var e=[],i=o.Polyline._flat(t),n=0,s=t.length;n=2&&e[0]instanceof o.LatLng&&e[0].equals(e[i-1])&&e.pop(),e},_setLatLngs:function(t){o.Polyline.prototype._setLatLngs.call(this,t),o.Polyline._flat(this._latlngs)&&(this._latlngs=[this._latlngs])},_defaultShape:function(){return o.Polyline._flat(this._latlngs[0])?this._latlngs[0]:this._latlngs[0][0]},_clipPoints:function(){var t=this._renderer._bounds,e=this.options.weight,i=new o.Point(e,e);if(t=new o.Bounds(t.min.subtract(i),t.max.add(i)),this._parts=[],this._pxBounds&&this._pxBounds.intersects(t)){if(this.options.noClip)return void(this._parts=this._rings);for(var n,s=0,r=this._rings.length;s';var i=t.firstChild;return i.style.behavior="url(#default#VML)",i&&"object"==typeof i.adj}catch(n){return!1}}(),o.SVG.include(o.Browser.vml?{_initContainer:function(){this._container=o.DomUtil.create("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||o.Renderer.prototype._update.call(this)},_initPath:function(t){var e=t._container=o.SVG.create("shape");o.DomUtil.addClass(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=o.SVG.create("path"),e.appendChild(t._path),this._updateStyle(t)},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;o.DomUtil.remove(e),t.removeInteractiveTarget(e)},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,s=t._container;s.stroked=!!n.stroke,s.filled=!!n.fill,n.stroke?(e||(e=t._stroke=o.SVG.create("stroke")),s.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=o.Util.isArray(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(s.removeChild(e),t._stroke=null),n.fill?(i||(i=t._fill=o.SVG.create("fill")),s.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(s.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){o.DomUtil.toFront(t._container)},_bringToBack:function(t){o.DomUtil.toBack(t._container)}}:{}),o.Browser.vml&&(o.SVG.create=function(){try{return e.namespaces.add("lvml","urn:schemas-microsoft-com:vml"),function(t){return e.createElement("')}}catch(t){return function(t){return e.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}}()),o.Canvas=o.Renderer.extend({onAdd:function(){o.Renderer.prototype.onAdd.call(this),this._layers=this._layers||{},this._draw()},_initContainer:function(){var t=this._container=e.createElement("canvas");o.DomEvent.on(t,"mousemove",o.Util.throttle(this._onMouseMove,32,this),this).on(t,"click dblclick mousedown mouseup contextmenu",this._onClick,this).on(t,"mouseout",this._handleMouseOut,this),this._ctx=t.getContext("2d")},_update:function(){if(!this._map._animatingZoom||!this._bounds){this._drawnLayers={},o.Renderer.prototype._update.call(this);var t=this._bounds,e=this._container,i=t.getSize(),n=o.Browser.retina?2:1;o.DomUtil.setPosition(e,t.min),e.width=n*i.x,e.height=n*i.y,e.style.width=i.x+"px",e.style.height=i.y+"px",o.Browser.retina&&this._ctx.scale(2,2),this._ctx.translate(-t.min.x,-t.min.y)}},_initPath:function(t){this._updateDashArray(t),this._layers[o.stamp(t)]=t},_addPath:o.Util.falseFn,_removePath:function(t){t._removed=!0,this._requestRedraw(t)},_updatePath:function(t){this._redrawBounds=t._pxBounds,this._draw(!0),t._project(),t._update(),this._draw(),this._redrawBounds=null},_updateStyle:function(t){this._updateDashArray(t),this._requestRedraw(t)},_updateDashArray:function(t){if(t.options.dashArray){var e,i=t.options.dashArray.split(","),n=[];for(e=0;et.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(u=!u);return u||o.Polyline.prototype._containsPoint.call(this,t,!0)},o.CircleMarker.prototype._containsPoint=function(t){return t.distanceTo(this._point)<=this._radius+this._clickTolerance()},o.GeoJSON=o.FeatureGroup.extend({initialize:function(t,e){o.setOptions(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,s=o.Util.isArray(t)?t:t.features;if(s){for(e=0,i=s.length;e100&&n<500||t.target._simulatedClick&&!t._simulated?void o.DomEvent.stop(t):(o.DomEvent._lastClick=i,void e(t))}},o.DomEvent.addListener=o.DomEvent.on,o.DomEvent.removeListener=o.DomEvent.off,o.Draggable=o.Evented.extend({options:{clickTolerance:3},statics:{START:o.Browser.touch?["touchstart","mousedown"]:["mousedown"],END:{mousedown:"mouseup",touchstart:"touchend",pointerdown:"touchend",MSPointerDown:"touchend"},MOVE:{mousedown:"mousemove",touchstart:"touchmove",pointerdown:"touchmove",MSPointerDown:"touchmove"}},initialize:function(t,e,i){this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(o.DomEvent.on(this._dragStartTarget,o.Draggable.START.join(" "),this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(o.DomEvent.off(this._dragStartTarget,o.Draggable.START.join(" "),this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){if(!t._simulated&&this._enabled&&(this._moved=!1,!o.DomUtil.hasClass(this._element,"leaflet-zoom-anim")&&!(o.Draggable._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches)&&this._enabled&&(o.Draggable._dragging=!0,this._preventOutline&&o.DomUtil.preventOutline(this._element),o.DomUtil.disableImageDrag(),o.DomUtil.disableTextSelection(),!this._moving))){this.fire("down");var i=t.touches?t.touches[0]:t;this._startPoint=new o.Point(i.clientX,i.clientY),o.DomEvent.on(e,o.Draggable.MOVE[t.type],this._onMove,this).on(e,o.Draggable.END[t.type],this._onUp,this)}},_onMove:function(i){if(!i._simulated&&this._enabled){if(i.touches&&i.touches.length>1)return void(this._moved=!0);var n=i.touches&&1===i.touches.length?i.touches[0]:i,s=new o.Point(n.clientX,n.clientY),r=s.subtract(this._startPoint);(r.x||r.y)&&(Math.abs(r.x)+Math.abs(r.y)50&&(this._positions.shift(),this._times.shift())}this._map.fire("move",t).fire("drag",t)},_onZoomEnd:function(){var t=this._map.getSize().divideBy(2),e=this._map.latLngToLayerPoint([0,0]);this._initialWorldOffset=e.subtract(t).x,this._worldWidth=this._map.getPixelWorldBounds().getSize().x},_viscousLimit:function(t,e){return t-(t-e)*this._viscosity},_onPreDragLimit:function(){if(this._viscosity&&this._offsetLimit){var t=this._draggable._newPos.subtract(this._draggable._startPos),e=this._offsetLimit;t.xe.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t)}},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,s=(n+e+i)%t-e-i,r=Math.abs(o+i)0?s:-s))-e;this._delta=0,this._startTime=null,r&&("center"===t.options.scrollWheelZoom?t.setZoom(e+r):t.setZoomAround(this._lastMousePos,e+r))}}),o.Map.addInitHook("addHandler","scrollWheelZoom",o.Map.ScrollWheelZoom),o.extend(o.DomEvent,{_touchstart:o.Browser.msPointer?"MSPointerDown":o.Browser.pointer?"pointerdown":"touchstart",_touchend:o.Browser.msPointer?"MSPointerUp":o.Browser.pointer?"pointerup":"touchend",addDoubleTapListener:function(t,e,i){function n(t){var e;if(e=o.Browser.pointer?o.DomEvent._pointersCount:t.touches.length,!(e>1)){var i=Date.now(),n=i-(r||i);a=t.touches?t.touches[0]:t,h=n>0&&n<=l,r=i}}function s(){if(h&&!a.cancelBubble){if(o.Browser.pointer){var t,i,n={};for(i in a)t=a[i],n[i]=t&&t.bind?t.bind(a):t;a=n}a.type="dblclick",e(a),r=null}}var r,a,h=!1,l=250,u="_leaflet_",c=this._touchstart,d=this._touchend;return t[u+c+i]=n,t[u+d+i]=s,t[u+"dblclick"+i]=e,t.addEventListener(c,n,!1),t.addEventListener(d,s,!1),o.Browser.edge||t.addEventListener("dblclick",e,!1),this},removeDoubleTapListener:function(t,e){var i="_leaflet_",n=t[i+this._touchstart+e],s=t[i+this._touchend+e],r=t[i+"dblclick"+e];return t.removeEventListener(this._touchstart,n,!1),t.removeEventListener(this._touchend,s,!1),o.Browser.edge||t.removeEventListener("dblclick",r,!1),this}}),o.extend(o.DomEvent,{POINTER_DOWN:o.Browser.msPointer?"MSPointerDown":"pointerdown",POINTER_MOVE:o.Browser.msPointer?"MSPointerMove":"pointermove",POINTER_UP:o.Browser.msPointer?"MSPointerUp":"pointerup",POINTER_CANCEL:o.Browser.msPointer?"MSPointerCancel":"pointercancel",TAG_WHITE_LIST:["INPUT","SELECT","OPTION"],_pointers:{},_pointersCount:0,addPointerListener:function(t,e,i,n){return"touchstart"===e?this._addPointerStart(t,i,n):"touchmove"===e?this._addPointerMove(t,i,n):"touchend"===e&&this._addPointerEnd(t,i,n),this},removePointerListener:function(t,e,i){var n=t["_leaflet_"+e+i];return"touchstart"===e?t.removeEventListener(this.POINTER_DOWN,n,!1):"touchmove"===e?t.removeEventListener(this.POINTER_MOVE,n,!1):"touchend"===e&&(t.removeEventListener(this.POINTER_UP,n,!1),t.removeEventListener(this.POINTER_CANCEL,n,!1)),this},_addPointerStart:function(t,i,n){var s=o.bind(function(t){if("mouse"!==t.pointerType&&t.pointerType!==t.MSPOINTER_TYPE_MOUSE){if(!(this.TAG_WHITE_LIST.indexOf(t.target.tagName)<0))return;o.DomEvent.preventDefault(t)}this._handlePointer(t,i)},this);if(t["_leaflet_touchstart"+n]=s,t.addEventListener(this.POINTER_DOWN,s,!1),!this._pointerDocListener){var r=o.bind(this._globalPointerUp,this);e.documentElement.addEventListener(this.POINTER_DOWN,o.bind(this._globalPointerDown,this),!0),e.documentElement.addEventListener(this.POINTER_MOVE,o.bind(this._globalPointerMove,this),!0),e.documentElement.addEventListener(this.POINTER_UP,r,!0),e.documentElement.addEventListener(this.POINTER_CANCEL,r,!0),this._pointerDocListener=!0}},_globalPointerDown:function(t){this._pointers[t.pointerId]=t,this._pointersCount++},_globalPointerMove:function(t){this._pointers[t.pointerId]&&(this._pointers[t.pointerId]=t)},_globalPointerUp:function(t){delete this._pointers[t.pointerId],this._pointersCount--},_handlePointer:function(t,e){t.touches=[];for(var i in this._pointers)t.touches.push(this._pointers[i]);t.changedTouches=[t],e(t)},_addPointerMove:function(t,e,i){var n=o.bind(function(t){(t.pointerType!==t.MSPOINTER_TYPE_MOUSE&&"mouse"!==t.pointerType||0!==t.buttons)&&this._handlePointer(t,e)},this);t["_leaflet_touchmove"+i]=n,t.addEventListener(this.POINTER_MOVE,n,!1)},_addPointerEnd:function(t,e,i){var n=o.bind(function(t){this._handlePointer(t,e)},this);t["_leaflet_touchend"+i]=n,t.addEventListener(this.POINTER_UP,n,!1),t.addEventListener(this.POINTER_CANCEL,n,!1)}}),o.Map.mergeOptions({touchZoom:o.Browser.touch&&!o.Browser.android23,bounceAtZoomLimits:!0}),o.Map.TouchZoom=o.Handler.extend({addHooks:function(){o.DomUtil.addClass(this._map._container,"leaflet-touch-zoom"),o.DomEvent.on(this._map._container,"touchstart",this._onTouchStart,this)},removeHooks:function(){o.DomUtil.removeClass(this._map._container,"leaflet-touch-zoom"),o.DomEvent.off(this._map._container,"touchstart",this._onTouchStart,this)},_onTouchStart:function(t){var i=this._map;if(t.touches&&2===t.touches.length&&!i._animatingZoom&&!this._zooming){var n=i.mouseEventToContainerPoint(t.touches[0]),s=i.mouseEventToContainerPoint(t.touches[1]);this._centerPoint=i.getSize()._divideBy(2),this._startLatLng=i.containerPointToLatLng(this._centerPoint),"center"!==i.options.touchZoom&&(this._pinchStartLatLng=i.containerPointToLatLng(n.add(s)._divideBy(2))),this._startDist=n.distanceTo(s),this._startZoom=i.getZoom(),this._moved=!1,this._zooming=!0,i._stop(),o.DomEvent.on(e,"touchmove",this._onTouchMove,this).on(e,"touchend",this._onTouchEnd,this),o.DomEvent.preventDefault(t)}},_onTouchMove:function(t){if(t.touches&&2===t.touches.length&&this._zooming){var e=this._map,i=e.mouseEventToContainerPoint(t.touches[0]),n=e.mouseEventToContainerPoint(t.touches[1]),s=i.distanceTo(n)/this._startDist;if(this._zoom=e.getScaleZoom(s,this._startZoom),!e.options.bounceAtZoomLimits&&(this._zoome.getMaxZoom()&&s>1)&&(this._zoom=e._limitZoom(this._zoom)),"center"===e.options.touchZoom){if(this._center=this._startLatLng,1===s)return}else{var r=i._add(n)._divideBy(2)._subtract(this._centerPoint);if(1===s&&0===r.x&&0===r.y)return;this._center=e.unproject(e.project(this._pinchStartLatLng,this._zoom).subtract(r),this._zoom)}this._moved||(e._moveStart(!0),this._moved=!0),o.Util.cancelAnimFrame(this._animRequest);var a=o.bind(e._move,e,this._center,this._zoom,{pinch:!0,round:!1});this._animRequest=o.Util.requestAnimFrame(a,this,!0),o.DomEvent.preventDefault(t)}},_onTouchEnd:function(){return this._moved&&this._zooming?(this._zooming=!1,o.Util.cancelAnimFrame(this._animRequest),o.DomEvent.off(e,"touchmove",this._onTouchMove).off(e,"touchend",this._onTouchEnd),void(this._map.options.zoomAnimation?this._map._animateZoom(this._center,this._map._limitZoom(this._zoom),!0,this._map.options.zoomSnap):this._map._resetView(this._center,this._map._limitZoom(this._zoom)))):void(this._zooming=!1)}}),o.Map.addInitHook("addHandler","touchZoom",o.Map.TouchZoom),o.Map.mergeOptions({tap:!0,tapTolerance:15}),o.Map.Tap=o.Handler.extend({addHooks:function(){o.DomEvent.on(this._map._container,"touchstart",this._onDown,this)},removeHooks:function(){o.DomEvent.off(this._map._container,"touchstart",this._onDown,this)},_onDown:function(t){if(t.touches){if(o.DomEvent.preventDefault(t),this._fireClick=!0,t.touches.length>1)return this._fireClick=!1,void clearTimeout(this._holdTimeout);var i=t.touches[0],n=i.target;this._startPos=this._newPos=new o.Point(i.clientX,i.clientY),n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.addClass(n,"leaflet-active"),this._holdTimeout=setTimeout(o.bind(function(){this._isTapValid()&&(this._fireClick=!1,this._onUp(),this._simulateEvent("contextmenu",i))},this),1e3),this._simulateEvent("mousedown",i),o.DomEvent.on(e,{touchmove:this._onMove,touchend:this._onUp},this)}},_onUp:function(t){if(clearTimeout(this._holdTimeout),o.DomEvent.off(e,{touchmove:this._onMove,touchend:this._onUp},this),this._fireClick&&t&&t.changedTouches){var i=t.changedTouches[0],n=i.target;n&&n.tagName&&"a"===n.tagName.toLowerCase()&&o.DomUtil.removeClass(n,"leaflet-active"),this._simulateEvent("mouseup",i),this._isTapValid()&&this._simulateEvent("click",i)}},_isTapValid:function(){return this._newPos.distanceTo(this._startPos)<=this._map.options.tapTolerance},_onMove:function(t){var e=t.touches[0];this._newPos=new o.Point(e.clientX,e.clientY),this._simulateEvent("mousemove",e)},_simulateEvent:function(i,n){var o=e.createEvent("MouseEvents");o._simulated=!0,n.target._simulatedClick=!0,o.initMouseEvent(i,!0,!0,t,1,n.screenX,n.screenY,n.clientX,n.clientY,!1,!1,!1,!1,0,null),n.target.dispatchEvent(o)}}),o.Browser.touch&&!o.Browser.pointer&&o.Map.addInitHook("addHandler","tap",o.Map.Tap),o.Map.mergeOptions({boxZoom:!0}),o.Map.BoxZoom=o.Handler.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane},addHooks:function(){o.DomEvent.on(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){o.DomEvent.off(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_resetState:function(){this._moved=!1},_onMouseDown:function(t){return!(!t.shiftKey||1!==t.which&&1!==t.button)&&(this._resetState(),o.DomUtil.disableTextSelection(),o.DomUtil.disableImageDrag(),this._startPoint=this._map.mouseEventToContainerPoint(t),void o.DomEvent.on(e,{contextmenu:o.DomEvent.stop,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this))},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=o.DomUtil.create("div","leaflet-zoom-box",this._container),o.DomUtil.addClass(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var e=new o.Bounds(this._point,this._startPoint),i=e.getSize();o.DomUtil.setPosition(this._box,e.min),this._box.style.width=i.x+"px",this._box.style.height=i.y+"px"},_finish:function(){this._moved&&(o.DomUtil.remove(this._box),o.DomUtil.removeClass(this._container,"leaflet-crosshair")),o.DomUtil.enableTextSelection(),o.DomUtil.enableImageDrag(),o.DomEvent.off(e,{contextmenu:o.DomEvent.stop,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){if((1===t.which||1===t.button)&&(this._finish(),this._moved)){setTimeout(o.bind(this._resetState,this),0);var e=new o.LatLngBounds(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point));this._map.fitBounds(e).fire("boxzoomend",{boxZoomBounds:e})}},_onKeyDown:function(t){27===t.keyCode&&this._finish()}}),o.Map.addInitHook("addHandler","boxZoom",o.Map.BoxZoom),o.Map.mergeOptions({keyboard:!0,keyboardPanDelta:80}),o.Map.Keyboard=o.Handler.extend({keyCodes:{left:[37],right:[39],down:[40],up:[38],zoomIn:[187,107,61,171],zoomOut:[189,109,54,173]},initialize:function(t){this._map=t,this._setPanDelta(t.options.keyboardPanDelta),this._setZoomDelta(t.options.zoomDelta)},addHooks:function(){var t=this._map._container;t.tabIndex<=0&&(t.tabIndex="0"),o.DomEvent.on(t,{focus:this._onFocus,blur:this._onBlur,mousedown:this._onMouseDown},this),this._map.on({focus:this._addHooks,blur:this._removeHooks},this)},removeHooks:function(){this._removeHooks(),o.DomEvent.off(this._map._container,{focus:this._onFocus,blur:this._onBlur,mousedown:this._onMouseDown},this),this._map.off({focus:this._addHooks,blur:this._removeHooks},this)},_onMouseDown:function(){if(!this._focused){var i=e.body,n=e.documentElement,o=i.scrollTop||n.scrollTop,s=i.scrollLeft||n.scrollLeft;this._map._container.focus(),t.scrollTo(s,o)}},_onFocus:function(){this._focused=!0,this._map.fire("focus")},_onBlur:function(){this._focused=!1,this._map.fire("blur")},_setPanDelta:function(t){var e,i,n=this._panKeys={},o=this.keyCodes;for(e=0,i=o.left.length;e0&&t.screenY>0&&this._map.getContainer().focus()}}),o.control=function(t){return new o.Control(t)},o.Map.include({addControl:function(t){return t.addTo(this),this},removeControl:function(t){return t.remove(),this},_initControlPos:function(){function t(t,s){var r=i+t+" "+i+s;e[t+s]=o.DomUtil.create("div",r,n)}var e=this._controlCorners={},i="leaflet-",n=this._controlContainer=o.DomUtil.create("div",i+"control-container",this._container);t("top","left"),t("top","right"),t("bottom","left"),t("bottom","right")},_clearControlPos:function(){o.DomUtil.remove(this._controlContainer)}}),o.Control.Zoom=o.Control.extend({options:{position:"topleft",zoomInText:"+",zoomInTitle:"Zoom in",zoomOutText:"-",zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=o.DomUtil.create("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,s){var r=o.DomUtil.create("a",i,n);return r.innerHTML=t,r.href="#",r.title=e,o.DomEvent.on(r,"mousedown dblclick",o.DomEvent.stopPropagation).on(r,"click",o.DomEvent.stop).on(r,"click",s,this).on(r,"click",this._refocusOnMap,this),r},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";o.DomUtil.removeClass(this._zoomInButton,e),o.DomUtil.removeClass(this._zoomOutButton,e),(this._disabled||t._zoom===t.getMinZoom())&&o.DomUtil.addClass(this._zoomOutButton,e),(this._disabled||t._zoom===t.getMaxZoom())&&o.DomUtil.addClass(this._zoomInButton,e)}}),o.Map.mergeOptions({zoomControl:!0}),o.Map.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new o.Control.Zoom,this.addControl(this.zoomControl))}),o.control.zoom=function(t){return new o.Control.Zoom(t)},o.Control.Attribution=o.Control.extend({options:{position:"bottomright",prefix:'Leaflet'},initialize:function(t){o.setOptions(this,t),this._attributions={}},onAdd:function(t){t.attributionControl=this,this._container=o.DomUtil.create("div","leaflet-control-attribution"),o.DomEvent&&o.DomEvent.disableClickPropagation(this._container);for(var e in t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),this._container},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t?(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update(),this):this},removeAttribution:function(t){return t?(this._attributions[t]&&(this._attributions[t]--,this._update()),this):this},_update:function(){if(this._map){ +var t=[];for(var e in this._attributions)this._attributions[e]&&t.push(e);var i=[];this.options.prefix&&i.push(this.options.prefix),t.length&&i.push(t.join(", ")),this._container.innerHTML=i.join(" | ")}}}),o.Map.mergeOptions({attributionControl:!0}),o.Map.addInitHook(function(){this.options.attributionControl&&(new o.Control.Attribution).addTo(this)}),o.control.attribution=function(t){return new o.Control.Attribution(t)},o.Control.Scale=o.Control.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=o.DomUtil.create("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=o.DomUtil.create("div",e,i)),t.imperial&&(this._iScale=o.DomUtil.create("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,i=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(i)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t),i=e<1e3?e+" m":e/1e3+" km";this._updateScale(this._mScale,i,e/t)},_updateImperial:function(t){var e,i,n,o=3.2808399*t;o>5280?(e=o/5280,i=this._getRoundNum(e),this._updateScale(this._iScale,i+" mi",i/e)):(n=this._getRoundNum(o),this._updateScale(this._iScale,n+" ft",n/o))},_updateScale:function(t,e,i){t.style.width=Math.round(this.options.maxWidth*i)+"px",t.innerHTML=e},_getRoundNum:function(t){var e=Math.pow(10,(Math.floor(t)+"").length-1),i=t/e;return i=i>=10?10:i>=5?5:i>=3?3:i>=2?2:1,e*i}}),o.control.scale=function(t){return new o.Control.Scale(t)},o.Control.Layers=o.Control.extend({options:{collapsed:!0,position:"topright",autoZIndex:!0,hideSingleBase:!1},initialize:function(t,e,i){o.setOptions(this,i),this._layers=[],this._lastZIndex=0,this._handlingClick=!1;for(var n in t)this._addLayer(t[n],n);for(n in e)this._addLayer(e[n],n,!0)},onAdd:function(t){return this._initLayout(),this._update(),this._map=t,t.on("zoomend",this._checkDisabledLayers,this),this._container},onRemove:function(){this._map.off("zoomend",this._checkDisabledLayers,this);for(var t=0;t1,this._baseLayersList.style.display=t?"":"none"),this._separator.style.display=e&&t?"":"none",this},_onLayerChange:function(t){this._handlingClick||this._update();var e=this._getLayer(o.stamp(t.target)),i=e.overlay?"add"===t.type?"overlayadd":"overlayremove":"add"===t.type?"baselayerchange":null;i&&this._map.fire(i,e)},_createRadioElement:function(t,i){var n='",o=e.createElement("div");return o.innerHTML=n,o.firstChild},_addItem:function(t){var i,n=e.createElement("label"),s=this._map.hasLayer(t.layer);t.overlay?(i=e.createElement("input"),i.type="checkbox",i.className="leaflet-control-layers-selector",i.defaultChecked=s):i=this._createRadioElement("leaflet-base-layers",s),i.layerId=o.stamp(t.layer),o.DomEvent.on(i,"click",this._onInputClick,this);var r=e.createElement("span");r.innerHTML=" "+t.name;var a=e.createElement("div");n.appendChild(a),a.appendChild(i),a.appendChild(r);var h=t.overlay?this._overlaysList:this._baseLayersList;return h.appendChild(n),this._checkDisabledLayers(),n},_onInputClick:function(){var t,e,i,n=this._form.getElementsByTagName("input"),o=[],s=[];this._handlingClick=!0;for(var r=n.length-1;r>=0;r--)t=n[r],e=this._getLayer(t.layerId).layer,i=this._map.hasLayer(e),t.checked&&!i?o.push(e):!t.checked&&i&&s.push(e);for(r=0;r=0;s--)t=n[s],e=this._getLayer(t.layerId).layer,t.disabled=e.options.minZoom!==i&&oe.options.maxZoom},_expand:function(){return this.expand()},_collapse:function(){return this.collapse()}}),o.control.layers=function(t,e,i){return new o.Control.Layers(t,e,i)},o.PosAnimation=o.Evented.extend({run:function(t,e,i,n){this.stop(),this._el=t,this._inProgress=!0,this._duration=i||.25,this._easeOutPower=1/Math.max(n||.5,.2),this._startPos=o.DomUtil.getPosition(t),this._offset=e.subtract(this._startPos),this._startTime=+new Date,this.fire("start"),this._animate()},stop:function(){this._inProgress&&(this._step(!0),this._complete())},_animate:function(){this._animId=o.Util.requestAnimFrame(this._animate,this),this._step()},_step:function(t){var e=+new Date-this._startTime,i=1e3*this._duration;e=0&&this._onZoomTransitionEnd()},_nothingToAnimate:function(){return!this._container.getElementsByClassName("leaflet-zoom-animated").length},_tryAnimatedZoom:function(t,e,i){if(this._animatingZoom)return!0;if(i=i||{},!this._zoomAnimated||i.animate===!1||this._nothingToAnimate()||Math.abs(e-this._zoom)>this.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),s=this._getCenterOffset(t)._divideBy(1-1/n);return!(i.animate!==!0&&!this.getSize().contains(s))&&(o.Util.requestAnimFrame(function(){this._moveStart(!0)._animateZoom(t,e,!0)},this),!0)},_animateZoom:function(t,e,i,n){i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,o.DomUtil.addClass(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),setTimeout(o.bind(this._onZoomTransitionEnd,this),250)},_onZoomTransitionEnd:function(){this._animatingZoom&&(o.DomUtil.removeClass(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom),o.Util.requestAnimFrame(function(){this._moveEnd(!0)},this))}}:{}),o.Map.include({flyTo:function(t,e,n){function s(t){var e=t?-1:1,i=t?v:g,n=v*v-g*g+e*L*L*y*y,o=2*i*L*y,s=n/o,r=Math.sqrt(s*s+1)-s,a=r<1e-9?-18:Math.log(r);return a}function r(t){return(Math.exp(t)-Math.exp(-t))/2}function a(t){return(Math.exp(t)+Math.exp(-t))/2}function h(t){return r(t)/a(t)}function l(t){return g*(a(x)/a(x+P*t))}function u(t){return g*(a(x)*h(x+P*t)-r(x))/L}function c(t){return 1-Math.pow(1-t,1.5)}function d(){var i=(Date.now()-b)/T,n=c(i)*w;i<=1?(this._flyToFrame=o.Util.requestAnimFrame(d,this),this._move(this.unproject(_.add(m.subtract(_).multiplyBy(u(n)/y)),f),this.getScaleZoom(g/l(n),f),{flyTo:!0})):this._move(t,e)._moveEnd(!0)}if(n=n||{},n.animate===!1||!o.Browser.any3d)return this.setView(t,e,n);this._stop();var _=this.project(this.getCenter()),m=this.project(t),p=this.getSize(),f=this._zoom;t=o.latLng(t),e=e===i?f:e;var g=Math.max(p.x,p.y),v=g*this.getZoomScale(f,e),y=m.distanceTo(_)||1,P=1.42,L=P*P,x=s(0),b=Date.now(),w=(s(1)-x)/P,T=n.duration?1e3*n.duration:1e3*w*.8;return this._moveStart(!0),d.call(this),this},flyToBounds:function(t,e){var i=this._getBoundsCenterZoom(t,e);return this.flyTo(i.center,i.zoom,e)}}),o.Map.include({_defaultLocateOptions:{timeout:1e4,watch:!1},locate:function(t){if(t=this._locateOptions=o.extend({},this._defaultLocateOptions,t),!("geolocation"in navigator))return this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this;var e=o.bind(this._handleGeolocationResponse,this),i=o.bind(this._handleGeolocationError,this);return t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e=t.code,i=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout");this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+i+"."})},_handleGeolocationResponse:function(t){var e=t.coords.latitude,i=t.coords.longitude,n=new o.LatLng(e,i),s=n.toBounds(t.coords.accuracy),r=this._locateOptions;if(r.setView){var a=this.getBoundsZoom(s);this.setView(n,r.maxZoom?Math.min(a,r.maxZoom):a)}var h={latlng:n,bounds:s,timestamp:t.timestamp};for(var l in t.coords)"number"==typeof t.coords[l]&&(h[l]=t.coords[l]);this.fire("locationfound",h)}})}(window,document); \ No newline at end of file diff --git a/frontend/src/App.js b/frontend/src/App.js new file mode 100644 index 0000000..5c0c757 --- /dev/null +++ b/frontend/src/App.js @@ -0,0 +1,33 @@ +import React from 'react' +import { + BrowserRouter as Router, + Route +} from 'react-router-dom'; +import injectTapEventPlugin from 'react-tap-event-plugin'; +import { Provider } from 'react-redux'; +import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'; +import getMuiTheme from 'material-ui/styles/getMuiTheme'; +import theme from './style/theme'; +import store from './redux/store'; +import IndexContainer from './views/Index'; +import SpaceList from './views/SpaceList'; +import UrlListView from './views/UrlListView'; +import layout from './layout'; + +injectTapEventPlugin(); + +const App = () => ( + + + +
+ )} /> + )} /> + )} /> +
+
+
+
+); + +export default App; \ No newline at end of file diff --git a/frontend/src/App.test.js b/frontend/src/App.test.js new file mode 100644 index 0000000..b84af98 --- /dev/null +++ b/frontend/src/App.test.js @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +it('renders without crashing', () => { + const div = document.createElement('div'); + ReactDOM.render(, div); +}); diff --git a/frontend/src/api/config.js b/frontend/src/api/config.js new file mode 100644 index 0000000..982b184 --- /dev/null +++ b/frontend/src/api/config.js @@ -0,0 +1,5 @@ +export default { + api: { + url: '/api', + }, +}; diff --git a/frontend/src/components/EventList.jsx b/frontend/src/components/EventList.jsx new file mode 100644 index 0000000..2b27079 --- /dev/null +++ b/frontend/src/components/EventList.jsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { Table, TableBody, TableRow, TableRowColumn } + from 'material-ui/Table'; +import InfoIcon from 'material-ui/svg-icons/action/info-outline'; +import { actions as calendarActions, eventStruct } from '../redux/modules/calendar'; +import { spacedataStruct } from '../redux/modules/spacedata'; + +const mapStateToProps = state => ({ + events: state.calendars.items, + spacedata: state.spacedata, +}); + +class EventList extends React.Component { + static propTypes = { + events: React.PropTypes.arrayOf( + React.PropTypes.shape(eventStruct), + ), + fetchCalendars: React.PropTypes.func, + spacedata: spacedataStruct, + }; + + defaultProps = { + events: [], + }; + + componentWillMount() { + this.props.fetchCalendars(); + } + + formatDate = date => (date.format('DD.MM.YYYY')); + formatTime = date => (date.format('HH:mm')); + + render() { + return ( + + + {this.props.events + .filter(event => + ( + this.props.spacedata.filter.indexOf(event.space) !== -1 + || this.props.spacedata.filter.length === 0 + ) + ) + .map(event => ( + + + {this.formatDate(event.start)} + + + {event.wholeDayEvent ? null : this.formatTime(event.start)} + + + {event.summary || event.description} + + + {event.space} + + + {event.url && + + } + + + ))} + +
+ ); + } +} + +export default connect(mapStateToProps, { + ...calendarActions, +})(EventList); diff --git a/frontend/src/components/Map.jsx b/frontend/src/components/Map.jsx new file mode 100644 index 0000000..841d816 --- /dev/null +++ b/frontend/src/components/Map.jsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Map as LeafletMap, TileLayer } from 'react-leaflet'; +import { connect } from 'react-redux'; +import Marker from './Marker'; +import { actions as spaceDataActions, spacedataStruct } from '../redux/modules/spacedata'; + +const mapStateToProps = state => ({ + spacedata: state.spacedata, +}); + +const mapDispatchToProps = { + ...spaceDataActions, +}; + +class Map extends React.Component { + static propTypes = { + spacedata: spacedataStruct.isRequired, + fetchSpacedata: React.PropTypes.func.isRequired, + toggleFilterSpacedata: React.PropTypes.func.isRequired, + }; + + componentWillMount() { + this.props.fetchSpacedata(); + } + + render() { + const centerGermany = [51.163375, 10.447683]; + return ( + + + {this.props.spacedata.items.map( + spacedata => ( + + ) + )} + + ); + } +} + +export default connect(mapStateToProps, mapDispatchToProps)(Map); diff --git a/frontend/src/components/Marker.jsx b/frontend/src/components/Marker.jsx new file mode 100644 index 0000000..9fb89ae --- /dev/null +++ b/frontend/src/components/Marker.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { CircleMarker, Popup } from 'react-leaflet'; +import theme from '../style/theme'; +import { spacedataElementStruct } from '../redux/modules/spacedata'; + +const Marker = (props) => { + const color = props.highlight ? theme.palette.accent2Color : theme.palette.primary1Color; + + const style = { + container: { + display: 'flex', + }, + logo: { + width: '50px', + marginRight: '5px', + }, + }; + + return ( + + +
+
+ {props.spacedata.space} +
+ + {props.spacedata.url} + +
+
+
+
+ ); +}; + +Marker.propTypes = { + spacedata: spacedataElementStruct.isRequired, + highlight: React.PropTypes.bool.isRequired, +}; + +export default Marker; diff --git a/frontend/src/components/SpaceApiInput.jsx b/frontend/src/components/SpaceApiInput.jsx new file mode 100644 index 0000000..788c6a5 --- /dev/null +++ b/frontend/src/components/SpaceApiInput.jsx @@ -0,0 +1,106 @@ +import React from 'react'; +import request from 'superagent'; +import TextField from 'material-ui/TextField'; +import FloatingActionButton from 'material-ui/FloatingActionButton'; +import ContentAdd from 'material-ui/svg-icons/content/add'; +import Snackbar from 'material-ui/Snackbar'; +import config from '../api/config'; + +class SpaceApiInput extends React.Component { + static propTypes = { + style: React.PropTypes.shape({}), + }; + + static defaultProps = { + style: {}, + }; + + state = { + open: false + }; + + getStyle = () => ({ + formContainer: { + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + width: '100%', + paddingLeft: '20px', + paddingRight: '20px', + paddingBottom: '40px', + }, + container: { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + paddingTop: '50px', + }, + hint: { + color: 'white', + width: '100%', + maxWidth: '550px', + fontSize: '13px', + textAlign: 'center', + }, + }); + + handleInputChange = (event) => { + this.setState({ url: event.target.value, input: event.target }); + }; + + handleButtonClick = () => { + request + .post(`${config.api.url}/urls`) + .send({ + url: this.state.url, + }) + .set('Content-Type', 'application/json') + .end((err) => { + if (!err) { + this.spaceApiInput.input.value = ''; + this.setState({ open: true }); + } + }); + }; + + render() { + const style = this.getStyle(); + return ( +
+

+ Trage die API-URL deines Hackerspaces hier ein und wir werden sie nach + kurzer Prüfung freischalten. Bei Fragen oder Problemen wende dich an  + + {'lokal@ccc.de'} + . +

+
+ (this.spaceApiInput = ref)} + style={{ width: '100%', maxWidth: '340px' }} + /> + + + +
+ this.setState({ open: false })} + /> +
+ ); + } +} + +export default SpaceApiInput; diff --git a/frontend/src/components/SpaceList.jsx b/frontend/src/components/SpaceList.jsx new file mode 100644 index 0000000..e3d610d --- /dev/null +++ b/frontend/src/components/SpaceList.jsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { + Table, + TableBody, + TableRow, + TableRowColumn, +} from 'material-ui/Table'; +import { spacedataStruct } from '../redux/modules/spacedata'; + +export class SpaceList extends React.Component { + static propTypes = { + fetchSpacedata: React.PropTypes.func.isRequired, + spacedata: spacedataStruct, + }; + + static defaultProps = { + spacedata: { + items: [], + }, + }; + + componentWillMount() { + this.props.fetchSpacedata(); + } + + render() { + const items = this.props.spacedata.items.sort( + (a, b) => a.space.toUpperCase().localeCompare(b.space.toUpperCase()) + ); + return ( + + + {items + .map(space => ( + + + {space.space} + + + + {space.url} + + + + ) + )} + +
+ ); + } +} + +export default SpaceList; diff --git a/frontend/src/components/Toolbar.jsx b/frontend/src/components/Toolbar.jsx new file mode 100644 index 0000000..a627ddd --- /dev/null +++ b/frontend/src/components/Toolbar.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { + Toolbar as MuiToolbar, + ToolbarGroup, + ToolbarTitle, +} from 'material-ui/Toolbar'; +import IconMenu from 'material-ui/IconMenu'; +import IconButton from 'material-ui/IconButton'; +import NavigationExpandMoreIcon from 'material-ui/svg-icons/navigation/expand-more'; +import MenuItem from 'material-ui/MenuItem'; +import { Link } from 'react-router-dom'; + +const Toolbar = () => ( + + + + + + + } + > + } + /> + } + /> + + + + +); + +export default Toolbar; diff --git a/frontend/src/components/UrlList.jsx b/frontend/src/components/UrlList.jsx new file mode 100644 index 0000000..2af23a7 --- /dev/null +++ b/frontend/src/components/UrlList.jsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { + Table, + TableBody, + TableRow, + TableRowColumn, +} from 'material-ui/Table'; +import TextField from 'material-ui/TextField'; +import FlatButton from 'material-ui/FlatButton'; +import moment from 'moment'; +import { spaceUrlStruct } from '../redux/modules/spaceurl'; + +export class UrlList extends React.Component { + static propTypes = { + fetchSpaceUrl: React.PropTypes.func.isRequired, + validateSpaceUrl: React.PropTypes.func.isRequired, + spaceurls: spaceUrlStruct, + }; + + static defaultProps = { + spaceurls: { + items: [], + }, + }; + + componentWillMount() { + this.props.fetchSpaceUrl(); + } + + getFormatedDateTime = timestamp => ( + moment + .unix(timestamp) + .format('DD.MM.YYYY HH:mm') + ); + + validateSpaceUrl = (spaceUrl) => { + const validatedSpaceUrl = { + url: spaceUrl.url, + validated: true, + }; + this.props.validateSpaceUrl(validatedSpaceUrl, this.secretInput.input.value); + }; + + render() { + return ( +
+ + + {this.props.spaceurls.items + .map(spaceurl => ( + + + + {spaceurl.url} + + + + {this.getFormatedDateTime(spaceurl.lastUpdated)} + + + {!spaceurl.validated ? this.validateSpaceUrl(spaceurl)} + primary + /> : null} + + + ) + )} + +
+ (this.secretInput = ref)} + /> +
+ ); + } +} + +export default UrlList; diff --git a/frontend/src/containers/SpaceList.jsx b/frontend/src/containers/SpaceList.jsx new file mode 100644 index 0000000..7d48671 --- /dev/null +++ b/frontend/src/containers/SpaceList.jsx @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { actions as spaceActions } from '../redux/modules/spacedata'; +import Component from '../components/SpaceList'; + +const mapStateToProps = state => ({ + spacedata: state.spacedata, +}); + +const mapDispatchToProps = { + ...spaceActions, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/frontend/src/containers/UrlList.jsx b/frontend/src/containers/UrlList.jsx new file mode 100644 index 0000000..775c57e --- /dev/null +++ b/frontend/src/containers/UrlList.jsx @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { actions as spaceUrlActions } from '../redux/modules/spaceurl'; +import Component from '../components/UrlList'; + +const mapStateToProps = state => ({ + spaceurls: state.spaceurls, +}); + +const mapDispatchToProps = { + ...spaceUrlActions, +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/frontend/src/index.js b/frontend/src/index.js new file mode 100644 index 0000000..2e970d0 --- /dev/null +++ b/frontend/src/index.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +import './style/css/reset.css'; +import './style/css/style.css'; + +ReactDOM.render( + , + document.getElementById('root') +); diff --git a/frontend/src/layout/index.jsx b/frontend/src/layout/index.jsx new file mode 100644 index 0000000..cd07932 --- /dev/null +++ b/frontend/src/layout/index.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import Toolbar from '../components/Toolbar'; + +const Layout = children => ( + () => ( +
+ + {children} +
+ ) +); + +export default Layout; diff --git a/frontend/src/redux/modules/calendar.js b/frontend/src/redux/modules/calendar.js new file mode 100644 index 0000000..ab8766f --- /dev/null +++ b/frontend/src/redux/modules/calendar.js @@ -0,0 +1,84 @@ +import { PropTypes } from 'react'; +import request from 'superagent'; +import flatten from 'lodash/flatten'; +import moment from 'moment'; +import RRule from 'rrule'; +import { createAction, handleActions } from 'redux-actions'; +import config from '../../api/config'; + +export const eventStruct = { + start: PropTypes.string.isRequired, + WholeDayEvent: PropTypes.bool.isRequired, + Description: PropTypes.string.isRequired, + Summary: PropTypes.string.isRequired, + space: PropTypes.string.isRequired, +}; + +const CALENDARS_FETCHED = 'CALENDARS_FETCHED'; + +export const fetched = createAction(CALENDARS_FETCHED, result => result); + +export const fetchCalendars = () => (dispatch) => { + request + .get(`${config.api.url}/calendar`) + .set('Content-Type', 'application/json') + .end( + (err, res) => { + if (!err) { + dispatch(fetched(res.body)); + } + } + ); +}; + +export const actions = { + fetchCalendars, +}; + +export default handleActions({ + [CALENDARS_FETCHED]: (state, { payload }) => { + const items = flatten(flatten( + payload.map( + calendar => ( + calendar.Events.map((event) => { + if (event.rrule) { + try { + const options = RRule.parseString(event.rrule); + options.dtstart = moment(event.start).toDate(); + const rule = new RRule(options); + + return rule.between( + moment().toDate(), + moment().add(3, 'months').toDate() + ).map(date => ( + { + ...event, + space: calendar.Space, + start: moment(date), + end: null, + } + )); + } + catch (ex) { + console.log(ex); + return []; + } + } + + return [ + { + ...event, + space: calendar.Space, + start: moment(event.start), + end: moment(event.end), + }, + ]; + }) + ) + ) + )) + .filter(event => event.start.isAfter()) + .sort((a, b) => a.start - b.start); + return { items }; + }, +}, { items: [] }); diff --git a/frontend/src/redux/modules/spacedata.js b/frontend/src/redux/modules/spacedata.js new file mode 100644 index 0000000..9c66379 --- /dev/null +++ b/frontend/src/redux/modules/spacedata.js @@ -0,0 +1,72 @@ +import { PropTypes } from 'react'; +import request from 'superagent'; +import { createAction, handleActions } from 'redux-actions'; +import config from '../../api/config'; + +export const filterStruct = PropTypes.arrayOf(PropTypes.string); +export const spacedataElementStruct = PropTypes.shape({ + space: PropTypes.string.isRequired, + location: PropTypes.shape({ + lat: PropTypes.number.isRequired, + lon: PropTypes.number.isRequired, + }), +}); +export const itemsStruct = PropTypes.arrayOf(spacedataElementStruct); +export const spacedataStruct = PropTypes.shape({ + items: itemsStruct, + filter: filterStruct, +}); + +const SPACEDATA_FETCHED = 'SPACEDATA_FETCHED'; +const SPACEDATA_FILTER = 'SPACEDATA_FILTER'; + +export const fetched = createAction(SPACEDATA_FETCHED, result => result); +export const filter = createAction(SPACEDATA_FILTER, result => result); + +export const fetchSpacedata = () => (dispatch) => { + request + .get(`${config.api.url}/spaces`) + .set('Content-Type', 'application/json') + .end( + (err, res) => { + if (!err) { + dispatch(fetched(res.body)); + } + } + ); +}; + +export const toggleFilterSpacedata = space => (dispatch) => { + dispatch( + filter( + { + space, + } + ) + ); +}; + +export const actions = { + fetchSpacedata, + toggleFilterSpacedata, +}; + +export default handleActions({ + [SPACEDATA_FETCHED]: (state, { payload }) => ( + { + ...state, + items: payload, + } + ), + [SPACEDATA_FILTER]: (state, { payload }) => { + const newState = { ...state }; + + if (newState.filter.indexOf(payload.space) === -1) { + newState.filter.push(payload.space); + } else { + newState.filter.splice(newState.filter.indexOf(payload.space), 1); + } + + return newState; + }, +}, { items: [], filter: [] }); diff --git a/frontend/src/redux/modules/spaceurl.js b/frontend/src/redux/modules/spaceurl.js new file mode 100644 index 0000000..ea9350a --- /dev/null +++ b/frontend/src/redux/modules/spaceurl.js @@ -0,0 +1,70 @@ +import { PropTypes } from 'react'; +import request from 'superagent'; +import { createAction, handleActions } from 'redux-actions'; +import config from '../../api/config'; + +export const itemStruct = PropTypes.shape({ + url: PropTypes.string.isRequired, + validated: PropTypes.bool.isRequired, + lastUpdated: PropTypes.number.isRequired, +}); +export const spaceUrlStruct = PropTypes.shape({ + items: PropTypes.arrayOf(itemStruct), +}); + +const SPACEURL_FETCHED = 'SPACEURL_FETCHED'; +const SPACEURL_VALIDATE = 'SPACEURL_VALIDATE'; + +export const fetched = createAction(SPACEURL_FETCHED, result => result); +export const validate = createAction(SPACEURL_VALIDATE, result => result); + +export const fetchSpaceUrl = () => (dispatch) => { + request + .get(`${config.api.url}/urls`) + .set('Content-Type', 'application/json') + .end( + (err, res) => { + if (!err) { + dispatch(fetched(res.body)); + } + } + ); +}; + +export const validateSpaceUrl = (spaceUrl, secret) => (dispatch) => { + request + .put(`${config.api.url}/urls/${secret}`) + .send(spaceUrl) + .set('Content-Type', 'application/json') + .end( + (err) => { + if (!err) { + dispatch(validate(spaceUrl)); + } + } + ); +}; + +export const actions = { + fetchSpaceUrl, + validateSpaceUrl, +}; + +export default handleActions({ + [SPACEURL_FETCHED]: (state, { payload }) => ({ + ...state, + items: payload, + }), + [SPACEURL_VALIDATE]: (state, { payload }) => { + const newState = { + ...state, + }; + + newState.items.forEach(spaceUrl => ({ + ...spaceUrl, + validated: spaceUrl.url === payload.url ? true : spaceUrl.validated, + })); + + return newState; + }, +}, { items: [] }); diff --git a/frontend/src/redux/rootReducer.js b/frontend/src/redux/rootReducer.js new file mode 100644 index 0000000..32b20f2 --- /dev/null +++ b/frontend/src/redux/rootReducer.js @@ -0,0 +1,10 @@ +import { combineReducers } from 'redux'; +import spacedataReducer from './modules/spacedata'; +import calendarsReducer from './modules/calendar'; +import spaceUrlsReducer from './modules/spaceurl'; + +export default combineReducers({ + spacedata: spacedataReducer, + calendars: calendarsReducer, + spaceurls: spaceUrlsReducer, +}); diff --git a/frontend/src/redux/store.js b/frontend/src/redux/store.js new file mode 100644 index 0000000..1642cc3 --- /dev/null +++ b/frontend/src/redux/store.js @@ -0,0 +1,15 @@ +import { createStore, compose, applyMiddleware } from 'redux'; +import thunk from 'redux-thunk'; +import rootReducer from './rootReducer'; + +const initialState = {}; +const store = createStore( + rootReducer, + initialState, + compose( + applyMiddleware(thunk) + ) +); + + +export default store; diff --git a/frontend/src/style/css/reset.css b/frontend/src/style/css/reset.css new file mode 100644 index 0000000..a99ee49 --- /dev/null +++ b/frontend/src/style/css/reset.css @@ -0,0 +1,4 @@ +html, body { + margin: 0; + padding: 0; +} \ No newline at end of file diff --git a/frontend/src/style/css/style.css b/frontend/src/style/css/style.css new file mode 100644 index 0000000..076d769 --- /dev/null +++ b/frontend/src/style/css/style.css @@ -0,0 +1,3 @@ +body { + background-color: #333333; +} \ No newline at end of file diff --git a/frontend/src/style/theme.js b/frontend/src/style/theme.js new file mode 100644 index 0000000..f0ad75c --- /dev/null +++ b/frontend/src/style/theme.js @@ -0,0 +1,23 @@ +import { fade } from 'material-ui/utils/colorManipulator'; +import { spacing, colors } from 'material-ui/styles'; + +export default { + spacing, + fontFamily: 'Roboto, sans-serif', + palette: { + primary1Color: colors.grey900, + primary2Color: colors.grey900, + primary3Color: colors.grey600, + accent1Color: colors.grey500, + accent2Color: colors.grey500, + accent3Color: colors.grey100, + textColor: colors.fullWhite, + secondaryTextColor: fade(colors.fullWhite, 0.7), + alternateTextColor: '#303030', + canvasColor: '#303030', + borderColor: fade(colors.fullWhite, 0.3), + disabledColor: fade(colors.fullWhite, 0.3), + pickerHeaderColor: fade(colors.fullWhite, 0.12), + clockCircleColor: fade(colors.fullWhite, 0.12), + }, +}; diff --git a/frontend/src/views/Index.jsx b/frontend/src/views/Index.jsx new file mode 100644 index 0000000..f83040b --- /dev/null +++ b/frontend/src/views/Index.jsx @@ -0,0 +1,21 @@ +import React from 'react'; +import Map from '../components/Map'; +import EventList from '../components/EventList'; + +const style = { + container: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, +}; + +const IndexContainer = () => ( +
+ + +
+); + +export default IndexContainer; diff --git a/frontend/src/views/SpaceList.jsx b/frontend/src/views/SpaceList.jsx new file mode 100644 index 0000000..7832518 --- /dev/null +++ b/frontend/src/views/SpaceList.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import SpaceList from '../containers/SpaceList'; +import SpaceApiInput from '../components/SpaceApiInput'; + +const SpaceListView = () => ( +
+ + +
+); + +export default SpaceListView; diff --git a/frontend/src/views/UrlListView.jsx b/frontend/src/views/UrlListView.jsx new file mode 100644 index 0000000..bf3de77 --- /dev/null +++ b/frontend/src/views/UrlListView.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import UrlList from '../containers/UrlList'; + +const UrlListView = () => ( +
+ +
+); + +export default UrlListView;