diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69dda6b2..ea99f16a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,68 @@
# Changelog
+## v1.5.0
+
+***Summary:***
+
+> - *changes and optimizations for making NEF_emulator capable of running bigger scenarios*
+> - *UE movement approach change:*
+> - *old: iterate over all path-points and simulate speed by using sleep() (LOW=1sec HIGH=0.1sec)*
+> - *new: constantly use sleep(1sec) and simulate speed by skipping (or not) path-points*
+> - *more on the pros/cons of this approach can be found at the relative source code section, the old one is commented out*
+> - *update of `leaflet.js` to version `1.8.0` (we've indetified a bug when closing mark tooltips, it's supposed to be fixed by the maintainers of the project at the upcoming release)*
+
+
+### UI changes
+
+ - `dashboard-cells.js` minor fix to display error details correctly in the toast message
+ - 🪛 fix `/map` js console errors caused by the UEs layer-checkbox (access to `null` marker)
+ - 🪛 fix `/map` UEs buttons: handle case of UEs with no paths assigned
+ - `/dashboard` page change: instead of 2 consecutive API requests on UE `Save` 👇:
+ - 1 API request to assign path everytime the user selects something different
+ - 1 API request on `Save` button
+ - `/map` page: add type-of-service column to datatable (cells now display `Monitoring Event API` or `AsSession With QoS API`)
+ - `/login`: add "hit enter --> submit form" functionality
+ - `/register`: add "hit enter --> submit form" functionality
+ - add `NEF` logo
+ - move part of `login.js` code to `app.js` (more clean approach + added `app.default_redirect` variable)
+ - `maps.js`: increase timeouts to 60 sec (edge case with >200 UEs, start/stop takes time)
+ - `maps.js`: add `api_get_moving_UEs()` to only retrieve moving UEs âž¡ move part of `ui_map_paint_UEs()` to `ui_map_paint_moving_UEs()`
+ - `app.js`: move `api_test_token()` outside document.ready() for quicker user auth checks
+ - `401` page redirect: when the token can't be validated the user is redirected to a 401 Unauthorized page and after a few seconds is redirected to `/login`. Previously, the user was redirected to login without being notified.
+ - `map.js`: optimize `helper_check_path_is_already_painted( path_id )` by replacing the simple array of painted paths with a key-value object
+
+
+### Backend
+
+ - ⛔ for optimization purposes, the UEs movement is handled in memory (no more intensive read/writes to Postgres) 👇
+ - âž• `api/v1/ue_movement/state-ues` now returns moving UEs information only. It helps with the edge cases of having many UEs and only a few of them actually moving around
+ - create new module/file for threads `utils.py` âž¡ `ue_movement.py`
+ - â›” `/utils/state-loop/{{supi}}` âž¡ `/ue_movement/state-loop/{{supi}}`
+ - â›” `/utils/start-loop` âž¡ `/ue_movement/start-loop`
+ - â›” `/utils/stop-loop` âž¡ `/ue_movement/stop-loop`
+ - `utils.py`: add a 2nd approach for making the UEs move within their path and control their speed (see #2eb19f8)
+ - `SQLAlchemy`: add `pool_size=150, max_overflow=20` to `create_engine( ... )`
+ - fix `NoneType` exception on MonitoringEvent one time request when cell is None
+ - Add middleware to return custom response header `X-Process-Time` that counts request-response proccesing time
+ - Split callbacks in two files 👉 From `send_callback.py` ➡ `monitoring_callbacks.py` + `qos_callback.py`
+ - fix callback notification for QoS after the transition from db to memory
+
+
+### Database
+
+ - postgreSQL add `command: -c shared_buffers=256MB -c max_connections=200` to `docker-compose`
+ - MonitoringEvent: migration from postgreSQL to MongoDB 👇
+ - fix `check_numberOfReports` function accordingly
+
+
+### Libraries
+
+ - upgrade `leaflet.js` (`1.7.1` to `1.8.0`)
+
+
+
+
+
## v1.4.1
### Migration to Python 3.9
@@ -90,6 +153,8 @@
+
+
## v1.3.2
- Fix UE-association-path selection with `path_id` 0 (no path selected) - both dashboard and backend
@@ -99,6 +164,11 @@
+
+
## âš™ Setup locally
**Host prerequisites**: `docker`, `docker-compose 1.29.2`, `build-essential`\*, `jq`\*\*
diff --git a/backend/app/app/api/api_v1/api.py b/backend/app/app/api/api_v1/api.py
index 73aa9621..3d7a22b7 100644
--- a/backend/app/app/api/api_v1/api.py
+++ b/backend/app/app/api/api_v1/api.py
@@ -1,16 +1,17 @@
from fastapi import APIRouter
-from app.api.api_v1.endpoints import paths, login, users, utils, gNB, Cell, UE, monitoringevent, qosMonitoring, qosInformation
+from app.api.api_v1 import endpoints
api_router = APIRouter()
-api_router.include_router(login.router, tags=["login"])
-api_router.include_router(users.router, prefix="/users", tags=["users"])
-api_router.include_router(utils.router, prefix="/utils", tags=["UI"])
-api_router.include_router(paths.router, prefix="/paths", tags=["Paths"])
-api_router.include_router(gNB.router, prefix="/gNBs", tags=["gNBs"])
-api_router.include_router(Cell.router, prefix="/Cells", tags=["Cells"])
-api_router.include_router(UE.router, prefix="/UEs", tags=["UEs"])
-api_router.include_router(qosInformation.router, prefix="/qosInfo", tags=["QoS Information"])
+api_router.include_router(endpoints.login.router, tags=["login"])
+api_router.include_router(endpoints.users.router, prefix="/users", tags=["users"])
+api_router.include_router(endpoints.utils.router, prefix="/utils", tags=["UI"])
+api_router.include_router(endpoints.ue_movement.router, prefix="/ue_movement", tags=["Movement"])
+api_router.include_router(endpoints.paths.router, prefix="/paths", tags=["Paths"])
+api_router.include_router(endpoints.gNB.router, prefix="/gNBs", tags=["gNBs"])
+api_router.include_router(endpoints.Cell.router, prefix="/Cells", tags=["Cells"])
+api_router.include_router(endpoints.UE.router, prefix="/UEs", tags=["UEs"])
+api_router.include_router(endpoints.qosInformation.router, prefix="/qosInfo", tags=["QoS Information"])
# api_router.include_router(monitoringevent.router, prefix="/3gpp-monitoring-event/v1", tags=["Monitoring Event API"])
# api_router.include_router(qosMonitoring.router, prefix="/3gpp-as-session-with-qos/v1", tags=["Session With QoS API"])
#api_router.include_router(monitoringevent.monitoring_callback_router, prefix="/3gpp-monitoring-event/v1", tags=["Monitoring Event API"])
@@ -18,7 +19,7 @@
# ---Create a subapp---
nef_router = APIRouter()
-nef_router.include_router(monitoringevent.router, prefix="/3gpp-monitoring-event/v1", tags=["Monitoring Event API"])
-nef_router.include_router(qosMonitoring.router, prefix="/3gpp-as-session-with-qos/v1", tags=["Session With QoS API"])
+nef_router.include_router(endpoints.monitoringevent.router, prefix="/3gpp-monitoring-event/v1", tags=["Monitoring Event API"])
+nef_router.include_router(endpoints.qosMonitoring.router, prefix="/3gpp-as-session-with-qos/v1", tags=["Session With QoS API"])
diff --git a/backend/app/app/api/api_v1/endpoints/__init__.py b/backend/app/app/api/api_v1/endpoints/__init__.py
index e69de29b..2caa584f 100644
--- a/backend/app/app/api/api_v1/endpoints/__init__.py
+++ b/backend/app/app/api/api_v1/endpoints/__init__.py
@@ -0,0 +1,11 @@
+from .login import router
+from .users import router
+from .utils import router
+from .ue_movement import router
+from .paths import router
+from .gNB import router
+from .Cell import router
+from .UE import router
+from .qosInformation import router
+from .qosMonitoring import router
+from .monitoringevent import router
\ No newline at end of file
diff --git a/backend/app/app/api/api_v1/endpoints/monitoringevent.py b/backend/app/app/api/api_v1/endpoints/monitoringevent.py
index 43355b35..d206941d 100644
--- a/backend/app/app/api/api_v1/endpoints/monitoringevent.py
+++ b/backend/app/app/api/api_v1/endpoints/monitoringevent.py
@@ -3,51 +3,41 @@
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse
from sqlalchemy.orm import Session
-
-from app import crud, models, schemas
+from pymongo.database import Database
+from app import models, schemas
+from app.crud import crud_mongo, user, ue
from app.api import deps
from app import tools
from app.api.api_v1.endpoints.utils import add_notifications
+from .ue_movement import retrieve_ue_state, retrieve_ue
router = APIRouter()
+db_collection= 'MonitoringEvent'
@router.get("/{scsAsId}/subscriptions", response_model=List[schemas.MonitoringEventSubscription], responses={204: {"model" : None}})
def read_active_subscriptions(
*,
scsAsId: str = Path(..., title="The ID of the Netapp that read all the subscriptions", example="myNetapp"),
- skip: int = 0,
- limit: int = 100,
- db: Session = Depends(deps.get_db),
+ db_mongo: Database = Depends(deps.get_mongo_db),
current_user: models.User = Depends(deps.get_current_active_user),
http_request: Request
) -> Any:
"""
Read all active subscriptions
"""
- if crud.user.is_superuser(current_user):
- subs = crud.monitoring.get_multi(db, skip=skip, limit=limit)
- else:
- subs = crud.monitoring.get_multi_by_owner(
- db=db, owner_id=current_user.id, skip=skip, limit=limit
- )
-
- json_subs = jsonable_encoder(subs)
- temp_json_subs = json_subs.copy() #Create copy of the list (json_subs) -> you cannot remove items from a list while you iterating the list.
+ retrieved_docs = crud_mongo.read_all(db_mongo, db_collection, current_user.id)
+ temp_json_subs = retrieved_docs.copy() #Create copy of the list (json_subs) -> you cannot remove items from a list while you iterating the list.
for sub in temp_json_subs:
sub_validate_time = tools.check_expiration_time(expire_time=sub.get("monitorExpireTime"))
if not sub_validate_time:
- crud.monitoring.remove(db=db, id=sub.get("id"))
- json_subs.remove(sub)
+ crud_mongo.delete_by_item(db_mongo, db_collection, "externalId", sub.get("externalId"))
+ retrieved_docs.remove(sub)
temp_json_subs.clear()
- if json_subs:
- for data in json_subs:
- data.pop("owner_id")
- data.pop("id")
-
- http_response = JSONResponse(content=json_subs, status_code=200)
+ if retrieved_docs:
+ http_response = JSONResponse(content=retrieved_docs, status_code=200)
add_notifications(http_request, http_response, False)
return http_response
else:
@@ -68,6 +58,7 @@ def create_subscription(
*,
scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"),
db: Session = Depends(deps.get_db),
+ db_mongo: Database = Depends(deps.get_mongo_db),
item_in: schemas.MonitoringEventSubscriptionCreate,
current_user: models.User = Depends(deps.get_current_active_user),
http_request: Request
@@ -75,43 +66,98 @@ def create_subscription(
"""
Create new subscription.
"""
- UE = crud.ue.get_externalId(db=db, externalId=str(item_in.externalId), owner_id=current_user.id)
+ UE = ue.get_externalId(db=db, externalId=str(item_in.externalId), owner_id=current_user.id)
if not UE:
raise HTTPException(status_code=409, detail="UE with this external identifier doesn't exist")
- if crud.monitoring.get_sub_externalId(db=db, externalId=item_in.externalId, owner_id=current_user.id):
- raise HTTPException(status_code=409, detail=f"There is already an active subscription for UE with external id {item_in.externalId}")
+ #One time request
if item_in.monitoringType == "LOCATION_REPORTING" and item_in.maximumNumberOfReports == 1:
json_compatible_item_data = {}
json_compatible_item_data["monitoringType"] = item_in.monitoringType
- json_compatible_item_data["locationInfo"] = {'cellId' : UE.Cell.cell_id, 'gNBId' : UE.Cell.gNB.gNB_id}
json_compatible_item_data["externalId"] = item_in.externalId
json_compatible_item_data["ipv4Addr"] = UE.ip_address_v4
-
+
+ if UE.Cell != None:
+ #If ue is moving retieve ue's information from memory else retrieve info from db
+ if retrieve_ue_state(supi=UE.supi, user_id=current_user.id):
+ cell_id_hex = retrieve_ue(UE.supi).get("cell_id_hex")
+ gnb_id_hex = retrieve_ue(UE.supi).get("gnb_id_hex")
+ json_compatible_item_data["locationInfo"] = {'cellId' : cell_id_hex, 'gNBId' : gnb_id_hex}
+ else:
+ json_compatible_item_data["locationInfo"] = {'cellId' : UE.Cell.cell_id, 'gNBId' : UE.Cell.gNB.gNB_id}
+ else:
+ json_compatible_item_data["locationInfo"] = {'cellId' : None, 'gNBId' : None}
+
http_response = JSONResponse(content=json_compatible_item_data, status_code=200)
add_notifications(http_request, http_response, False)
return http_response
+ #Subscription
elif item_in.monitoringType == "LOCATION_REPORTING" and item_in.maximumNumberOfReports>1:
- response = crud.monitoring.create_with_owner(db=db, obj_in=item_in, owner_id=current_user.id)
-
- json_compatible_item_data = jsonable_encoder(response)
- json_compatible_item_data.pop("owner_id")
- json_compatible_item_data.pop("id")
- link = str(http_request.url) + '/' + str(response.id)
- json_compatible_item_data["link"] = link
- json_compatible_item_data["ipv4Addr"] = UE.ip_address_v4
- crud.monitoring.update(db=db, db_obj=response, obj_in={"link" : link})
+ #Check if subscription with externalid exists
+ if crud_mongo.read_by_multiple_pairs(db_mongo, db_collection, externalId = item_in.externalId, monitoringType = item_in.monitoringType):
+ raise HTTPException(status_code=409, detail=f"There is already an active subscription for UE with external id {item_in.externalId} - Monitoring Type = {item_in.monitoringType}")
+
+ json_data = jsonable_encoder(item_in.dict(exclude_unset=True))
+ json_data.update({'owner_id' : current_user.id, "ipv4Addr" : UE.ip_address_v4})
+
+ inserted_doc = crud_mongo.create(db_mongo, db_collection, json_data)
+ #Create the reference resource and location header
+ link = str(http_request.url) + '/' + str(inserted_doc.inserted_id)
response_header = {"location" : link}
- http_response = JSONResponse(content=json_compatible_item_data, status_code=201, headers=response_header)
+
+ #Update the subscription with the new resource (link) and return the response (+response header)
+ crud_mongo.update_new_field(db_mongo, db_collection, inserted_doc.inserted_id, {"link" : link})
+
+ #Retrieve the updated document | UpdateResult is not a dict
+ updated_doc = crud_mongo.read_uuid(db_mongo, db_collection, inserted_doc.inserted_id)
+ updated_doc.pop("owner_id")
+
+
+ http_response = JSONResponse(content=updated_doc, status_code=201, headers=response_header)
add_notifications(http_request, http_response, False)
return http_response
+ elif item_in.monitoringType == "LOSS_OF_CONNECTIVITY" and item_in.maximumNumberOfReports == 1:
+ return JSONResponse(content=jsonable_encoder(
+ {
+ "title" : "The requested parameters are out of range",
+ "invalidParams" : {
+ "param" : "maximumNumberOfReports",
+ "reason" : "\"maximumNumberOfReports\" should be greater than 1 in case of LOSS_OF_CONNECTIVITY event"
+ }
+ }
+ ), status_code=403)
+ elif item_in.monitoringType == "LOSS_OF_CONNECTIVITY" and item_in.maximumNumberOfReports > 1:
+ #Check if subscription with externalid && monitoringType exists
+ if crud_mongo.read_by_multiple_pairs(db_mongo, db_collection, externalId = item_in.externalId, monitoringType = item_in.monitoringType):
+ raise HTTPException(status_code=409, detail=f"There is already an active subscription for UE with external id {item_in.externalId} - Monitoring Type = {item_in.monitoringType}")
+
+ json_data = jsonable_encoder(item_in.dict(exclude_unset=True))
+ json_data.update({'owner_id' : current_user.id, "ipv4Addr" : UE.ip_address_v4})
+
+ inserted_doc = crud_mongo.create(db_mongo, db_collection, json_data)
+
+ #Create the reference resource and location header
+ link = str(http_request.url) + '/' + str(inserted_doc.inserted_id)
+ response_header = {"location" : link}
+
+ #Update the subscription with the new resource (link) and return the response (+response header)
+ crud_mongo.update_new_field(db_mongo, db_collection, inserted_doc.inserted_id, {"link" : link})
+
+ #Retrieve the updated document | UpdateResult is not a dict
+ updated_doc = crud_mongo.read_uuid(db_mongo, db_collection, inserted_doc.inserted_id)
+ updated_doc.pop("owner_id")
+
+
+ http_response = JSONResponse(content=updated_doc, status_code=201, headers=response_header)
+ add_notifications(http_request, http_response, False)
+ return http_response
@router.put("/{scsAsId}/subscriptions/{subscriptionId}", response_model=schemas.MonitoringEventSubscription)
@@ -119,36 +165,42 @@ def update_subscription(
*,
scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"),
subscriptionId: str = Path(..., title="Identifier of the subscription resource"),
- db: Session = Depends(deps.get_db),
- item_in: schemas.MonitoringEventSubscription,
+ db_mongo: Database = Depends(deps.get_mongo_db),
+ item_in: schemas.MonitoringEventSubscriptionCreate,
current_user: models.User = Depends(deps.get_current_active_user),
http_request: Request
) -> Any:
"""
Update/Replace an existing subscription resource
"""
- id = int(subscriptionId)
+ try:
+ retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId)
+ except Exception as ex:
+ raise HTTPException(status_code=400, detail='Please enter a valid uuid (24-character hex string)')
- sub = crud.monitoring.get(db=db, id=int(subscriptionId))
- if not sub:
+ #Check if the document exists
+ if not retrieved_doc:
raise HTTPException(status_code=404, detail="Subscription not found")
- if not crud.user.is_superuser(current_user) and (sub.owner_id != current_user.id):
+ #If the document exists then validate the owner
+ if not user.is_superuser(current_user) and (retrieved_doc['owner_id'] != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
-
- sub_validate_time = tools.check_expiration_time(expire_time=sub.monitorExpireTime)
+
+ sub_validate_time = tools.check_expiration_time(expire_time=retrieved_doc.get("monitorExpireTime"))
if sub_validate_time:
- sub = crud.monitoring.update(db=db, db_obj=sub, obj_in=item_in)
-
- json_compatible_item_data = jsonable_encoder(sub)
- json_compatible_item_data.pop("owner_id")
- json_compatible_item_data.pop("id")
- http_response = JSONResponse(content=json_compatible_item_data, status_code=200)
+ #Update the document
+ json_data = jsonable_encoder(item_in)
+ crud_mongo.update_new_field(db_mongo, db_collection, subscriptionId, json_data)
+
+ #Retrieve the updated document | UpdateResult is not a dict
+ updated_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId)
+ updated_doc.pop("owner_id")
+ http_response = JSONResponse(content=updated_doc, status_code=200)
add_notifications(http_request, http_response, False)
return http_response
else:
- crud.monitoring.remove(db=db, id=id)
+ crud_mongo.delete_by_uuid(db_mongo, db_collection, subscriptionId)
raise HTTPException(status_code=403, detail="Subscription has expired")
@@ -157,33 +209,35 @@ def read_subscription(
*,
scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"),
subscriptionId: str = Path(..., title="Identifier of the subscription resource"),
- db: Session = Depends(deps.get_db),
+ db_mongo: Database = Depends(deps.get_mongo_db),
current_user: models.User = Depends(deps.get_current_active_user),
http_request: Request
) -> Any:
"""
Get subscription by id
"""
- id = int(subscriptionId)
- sub = crud.monitoring.get(db=db, id=id)
+ try:
+ retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId)
+ except Exception as ex:
+ raise HTTPException(status_code=400, detail='Please enter a valid uuid (24-character hex string)')
- if not sub:
+ #Check if the document exists
+ if not retrieved_doc:
raise HTTPException(status_code=404, detail="Subscription not found")
- if not crud.user.is_superuser(current_user) and (sub.owner_id != current_user.id):
+ #If the document exists then validate the owner
+ if not user.is_superuser(current_user) and (retrieved_doc['owner_id'] != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
- sub_validate_time = tools.check_expiration_time(expire_time=sub.monitorExpireTime)
+ sub_validate_time = tools.check_expiration_time(expire_time=retrieved_doc.get("monitorExpireTime"))
if sub_validate_time:
- json_compatible_item_data = jsonable_encoder(sub)
- json_compatible_item_data.pop("owner_id")
- json_compatible_item_data.pop("id")
- http_response = JSONResponse(content=json_compatible_item_data, status_code=200)
+ retrieved_doc.pop("owner_id")
+ http_response = JSONResponse(content=retrieved_doc, status_code=200)
add_notifications(http_request, http_response, False)
return http_response
else:
- crud.monitoring.remove(db=db, id=id)
+ crud_mongo.delete_by_uuid(db_mongo, db_collection, subscriptionId)
raise HTTPException(status_code=403, detail="Subscription has expired")
@router.delete("/{scsAsId}/subscriptions/{subscriptionId}", response_model=schemas.MonitoringEventSubscription)
@@ -191,33 +245,31 @@ def delete_subscription(
*,
scsAsId: str = Path(..., title="The ID of the Netapp that creates a subscription", example="myNetapp"),
subscriptionId: str = Path(..., title="Identifier of the subscription resource"),
- db: Session = Depends(deps.get_db),
+ db_mongo: Database = Depends(deps.get_mongo_db),
current_user: models.User = Depends(deps.get_current_active_user),
http_request: Request
) -> Any:
"""
Delete a subscription
"""
- sub = crud.monitoring.get(db=db, id=int(subscriptionId))
- if not sub:
+ try:
+ retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId)
+ except Exception as ex:
+ raise HTTPException(status_code=400, detail='Please enter a valid uuid (24-character hex string)')
+
+ #Check if the document exists
+ if not retrieved_doc:
raise HTTPException(status_code=404, detail="Subscription not found")
- if not crud.user.is_superuser(current_user) and (sub.owner_id != current_user.id):
+ #If the document exists then validate the owner
+ if not user.is_superuser(current_user) and (retrieved_doc['owner_id'] != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
+
+ crud_mongo.delete_by_uuid(db_mongo, db_collection, subscriptionId)
+ retrieved_doc.pop("owner_id")
+
+ http_response = JSONResponse(content=retrieved_doc, status_code=200)
+ add_notifications(http_request, http_response, False)
+ return http_response
- sub_validate_time = tools.check_expiration_time(expire_time=sub.monitorExpireTime)
-
- if sub_validate_time:
- sub = crud.monitoring.remove(db=db, id=int(subscriptionId))
-
- json_compatible_item_data = jsonable_encoder(sub)
- json_compatible_item_data.pop("owner_id")
- json_compatible_item_data.pop("id")
- http_response = JSONResponse(content=json_compatible_item_data, status_code=200)
-
- add_notifications(http_request, http_response, False)
- return http_response
- else:
- crud.monitoring.remove(db=db, id=id)
- raise HTTPException(status_code=403, detail="Subscription has expired")
diff --git a/backend/app/app/api/api_v1/endpoints/qosInformation.py b/backend/app/app/api/api_v1/endpoints/qosInformation.py
index f1d95cda..3ddfc74c 100644
--- a/backend/app/app/api/api_v1/endpoints/qosInformation.py
+++ b/backend/app/app/api/api_v1/endpoints/qosInformation.py
@@ -21,7 +21,7 @@ def qos_reference_match(qos_reference):
for q in qos_5qi:
if q.get('value') == qos_reference:
qos_characteristics = q.copy()
- print(f"Inside qos_reference_match at qosInformation.py {qos_characteristics}")
+ # print(f"Inside qos_reference_match at qosInformation.py {qos_characteristics}")
if not qos_characteristics:
raise HTTPException(status_code=400, detail=f"The 5QI (qosReference) {qos_reference} does not exist")
diff --git a/backend/app/app/api/api_v1/endpoints/qosMonitoring.py b/backend/app/app/api/api_v1/endpoints/qosMonitoring.py
index 5d7b5170..a8172320 100644
--- a/backend/app/app/api/api_v1/endpoints/qosMonitoring.py
+++ b/backend/app/app/api/api_v1/endpoints/qosMonitoring.py
@@ -116,11 +116,11 @@ def create_subscription(
response_header = {"location" : link}
#Update the subscription with the new resource (link) and return the response (+response header)
-
crud_mongo.update_new_field(db_mongo, db_collection, inserted_doc.inserted_id, {"link" : link})
+
+ #Retrieve the updated document | UpdateResult is not a dict
updated_doc = crud_mongo.read_uuid(db_mongo, db_collection, inserted_doc.inserted_id)
-
updated_doc.pop("owner_id") #Remove owner_id from the response
http_response = JSONResponse(content=updated_doc, status_code=201, headers=response_header)
@@ -189,10 +189,10 @@ def update_subscription(
json_data = jsonable_encoder(item_in)
crud_mongo.update_new_field(db_mongo, db_collection, subscriptionId, json_data)
- #Retrieve the updated document
- retrieved_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId)
- retrieved_doc.pop("owner_id")
- http_response = JSONResponse(content=retrieved_doc, status_code=200)
+ #Retrieve the updated document | UpdateResult is not a dict
+ updated_doc = crud_mongo.read_uuid(db_mongo, db_collection, subscriptionId)
+ updated_doc.pop("owner_id")
+ http_response = JSONResponse(content=updated_doc, status_code=200)
add_notifications(http_request, http_response, False)
return http_response
@@ -221,7 +221,7 @@ def delete_subscription(
if not user.is_superuser(current_user) and (retrieved_doc['owner_id'] != current_user.id):
raise HTTPException(status_code=400, detail="Not enough permissions")
- crud_mongo.delete(db_mongo, db_collection, subscriptionId)
+ crud_mongo.delete_by_uuid(db_mongo, db_collection, subscriptionId)
http_response = JSONResponse(content=retrieved_doc, status_code=200)
add_notifications(http_request, http_response, False)
return http_response
diff --git a/backend/app/app/api/api_v1/endpoints/ue_movement.py b/backend/app/app/api/api_v1/endpoints/ue_movement.py
new file mode 100644
index 00000000..af639083
--- /dev/null
+++ b/backend/app/app/api/api_v1/endpoints/ue_movement.py
@@ -0,0 +1,423 @@
+import threading, logging, time, requests
+from fastapi import APIRouter, Path, Depends, HTTPException
+from fastapi.encoders import jsonable_encoder
+from pymongo import MongoClient
+from typing import Any
+from app import crud, tools, models
+from app.crud import crud_mongo
+from app.tools.distance import check_distance
+from app.tools import qos_callback
+from app.db.session import SessionLocal
+from app.api import deps
+from app.schemas import Msg
+from app.tools import monitoring_callbacks, timer
+
+#Dictionary holding threads that are running per user id.
+threads = {}
+
+#Dictionary holding UEs' information
+ues = {}
+
+class BackgroundTasks(threading.Thread):
+
+ def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
+ super().__init__(group=group, target=target, name=name)
+ self._args = args
+ self._kwargs = kwargs
+ self._stop_threads = False
+ return
+
+ def run(self):
+
+ current_user = self._args[0]
+ supi = self._args[1]
+
+ try:
+ db = SessionLocal()
+ client = MongoClient("mongodb://mongo:27017", username='root', password='pass')
+ db_mongo = client.fastapi
+
+ #Initiate UE - if exists
+ UE = crud.ue.get_supi(db=db, supi=supi)
+ if not UE:
+ logging.warning("UE not found")
+ threads.pop(f"{supi}")
+ return
+ if (UE.owner_id != current_user.id):
+ logging.warning("Not enough permissions")
+ threads.pop(f"{supi}")
+ return
+
+ #Insert running UE in the dictionary
+
+ global ues
+ ues[f"{supi}"] = jsonable_encoder(UE)
+ ues[f"{supi}"].pop("id")
+
+ if UE.Cell_id != None:
+ ues[f"{supi}"]["cell_id_hex"] = UE.Cell.cell_id
+ ues[f"{supi}"]["gnb_id_hex"] = UE.Cell.gNB.gNB_id
+ else:
+ ues[f"{supi}"]["cell_id_hex"] = None
+ ues[f"{supi}"]["gnb_id_hex"] = None
+
+
+ #Retrieve paths & points
+ path = crud.path.get(db=db, id=UE.path_id)
+ if not path:
+ logging.warning("Path not found")
+ threads.pop(f"{supi}")
+ return
+ if (path.owner_id != current_user.id):
+ logging.warning("Not enough permissions")
+ threads.pop(f"{supi}")
+ return
+
+ points = crud.points.get_points(db=db, path_id=UE.path_id)
+ points = jsonable_encoder(points)
+
+ #Retrieve all the cells
+ Cells = crud.cell.get_multi_by_owner(db=db, owner_id=current_user.id, skip=0, limit=100)
+ json_cells = jsonable_encoder(Cells)
+
+ is_superuser = crud.user.is_superuser(current_user)
+
+ t = timer.Timer()
+ '''
+ ===================================================================
+ 2nd Approach for updating UEs position
+ ===================================================================
+
+ Summary: while(TRUE) --> keep increasing the moving index
+
+
+ points [ 1 2 3 4 5 6 7 8 9 10 ... ] . . . . . . .
+ ^ current index
+ ^ moving index ^ moving can also reach here
+
+ current: shows where the UE is
+ moving : starts within the range of len(points) and keeps increasing.
+ When it goes out of these bounds, the MOD( len(points) ) prevents
+ the "index out of range" exception. It also starts the iteration
+ of points from the begining, letting the UE moving in endless loops.
+
+ Sleep: in both LOW / HIGH speed cases, the thread sleeps for 1 sec
+
+ Speed: LOW : (moving_position_index += 1) no points are skipped, this means 1m/sec
+ HIGH: (moving_position_index += 10) skips 10 points, thus... ~10m/sec
+
+ Pros: + the UE position is updated once every sec (not very aggressive)
+ + we can easily set speed this way (by skipping X points --> X m/sec)
+ Cons: - skipping points and updating once every second decreases the event resolution
+
+ -------------------------------------------------------------------
+ '''
+
+ current_position_index = -1
+
+ # find the index of the point where the UE is located
+ for index, point in enumerate(points):
+ if (UE.latitude == point["latitude"]) and (UE.longitude == point["longitude"]):
+ current_position_index = index
+
+ # start iterating from this index and keep increasing the moving_position_index...
+ moving_position_index = current_position_index
+
+ while True:
+ try:
+ # UE = crud.ue.update_coordinates(db=db, lat=points[current_position_index]["latitude"], long=points[current_position_index]["longitude"], db_obj=UE)
+ # cell_now = check_distance(UE.latitude, UE.longitude, json_cells) #calculate the distance from all the cells
+ ues[f"{supi}"]["latitude"] = points[current_position_index]["latitude"]
+ ues[f"{supi}"]["longitude"] = points[current_position_index]["longitude"]
+ cell_now = check_distance(ues[f"{supi}"]["latitude"], ues[f"{supi}"]["longitude"], json_cells) #calculate the distance from all the cells
+ except Exception as ex:
+ logging.warning("Failed to update coordinates")
+ logging.warning(ex)
+
+ if cell_now != None:
+ try:
+ t.stop()
+ except timer.TimerError as ex:
+ # logging.critical(ex)
+ pass
+
+ # if UE.Cell_id != cell_now.get('id'): #Cell has changed in the db "handover"
+ if ues[f"{supi}"]["Cell_id"] != cell_now.get('id'): #Cell has changed in the db "handover"
+ # logging.warning(f"UE({UE.supi}) with ipv4 {UE.ip_address_v4} handovers to Cell {cell_now.get('id')}, {cell_now.get('description')}")
+ # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : cell_now.get('id')})
+ ues[f"{supi}"]["Cell_id"] = cell_now.get('id')
+ ues[f"{supi}"]["cell_id_hex"] = cell_now.get('cell_id')
+ gnb = crud.gnb.get(db=db, id=cell_now.get("gNB_id"))
+ ues[f"{supi}"]["gnb_id_hex"] = gnb.gNB_id
+
+ #Retrieve the subscription of the UE by external Id | This could be outside while true but then the user cannot subscribe when the loop runs
+ # sub = crud.monitoring.get_sub_externalId(db=db, externalId=UE.external_identifier, owner_id=current_user.id)
+ sub = crud_mongo.read(db_mongo, "MonitoringEvent", "externalId", UE.external_identifier)
+
+ #Validation of subscription
+ if not sub:
+ # logging.warning("Monitoring Event subscription not found")
+ pass
+ elif not is_superuser and (sub.get("owner_id") != current_user.id):
+ # logging.warning("Not enough permissions")
+ pass
+ else:
+ sub_validate_time = tools.check_expiration_time(expire_time=sub.get("monitorExpireTime"))
+ if sub_validate_time:
+ sub = tools.check_numberOfReports(db_mongo, sub)
+ if sub: #return the callback request only if subscription is valid
+ try:
+ response = monitoring_callbacks.location_callback(ues[f"{supi}"], sub.get("notificationDestination"), sub.get("link"))
+ # logging.info(response.json())
+ except requests.exceptions.ConnectionError as ex:
+ logging.warning("Failed to send the callback request")
+ logging.warning(ex)
+ crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id"))
+ continue
+ else:
+ crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id"))
+ logging.warning("Subscription has expired (expiration date)")
+
+ #QoS Monitoring Event (handover)
+ # ues_connected = crud.ue.get_by_Cell(db=db, cell_id=UE.Cell_id)
+ ues_connected = 0
+ # temp_ues = ues.copy()
+ # for ue in temp_ues:
+ # # print(ue)
+ # if ues[ue]["Cell_id"] == ues[f"{supi}"]["Cell_id"]:
+ # ues_connected += 1
+
+ #subtract 1 for the UE that is currently running. We are looking for other ues that are currently connected in the same cell
+ ues_connected -= 1
+
+ if ues_connected > 1:
+ gbr = 'QOS_NOT_GUARANTEED'
+ else:
+ gbr = 'QOS_GUARANTEED'
+
+ # logging.warning(gbr)
+ # qos_notification_control(gbr ,current_user, UE.ip_address_v4)
+ qos_callback.qos_notification_control(current_user, ues[f"{supi}"]["ip_address_v4"], ues.copy(), ues[f"{supi}"])
+
+ else:
+ # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : None})
+ try:
+ t.start()
+ except timer.TimerError as ex:
+ # logging.critical(ex)
+ pass
+
+ ues[f"{supi}"]["Cell_id"] = None
+ ues[f"{supi}"]["cell_id_hex"] = None
+ ues[f"{supi}"]["gnb_id_hex"] = None
+
+ # logging.info(f'User: {current_user.id} | UE: {supi} | Current location: latitude ={UE.latitude} | longitude = {UE.longitude} | Speed: {UE.speed}' )
+
+ if UE.speed == 'LOW':
+ # don't skip any points, keep default speed 1m /sec
+ moving_position_index += 1
+ elif UE.speed == 'HIGH':
+ # skip 10 points --> 10m / sec
+ moving_position_index += 10
+
+ time.sleep(1)
+
+ current_position_index = moving_position_index%(len(points))
+
+
+ if self._stop_threads:
+ logging.critical("Terminating thread...")
+ crud.ue.update_coordinates(db=db, lat=ues[f"{supi}"]["latitude"], long=ues[f"{supi}"]["longitude"], db_obj=UE)
+ crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : ues[f"{supi}"]["Cell_id"]})
+ ues.pop(f"{supi}")
+ break
+
+ # End of 2nd Approach for updating UEs position
+
+
+
+ '''
+ ===================================================================
+ 1st Approach for updating UEs position
+ ===================================================================
+
+ Summary: while(TRUE) --> keep iterating the points list again and again
+
+
+ points [ 1 2 3 4 5 6 7 8 9 10 ... ] . . . . . . .
+ ^ point
+ ^ flag
+
+ flag: it is used once to find the current UE position and then is
+ set to False
+
+ Sleep/
+ Speed: LOW : sleeps 1 sec and goes to the next point (1m/sec)
+ HIGH: sleeps 0.1 sec and goes to the next point (10m/sec)
+
+ Pros: + the UEs goes over every point and never skips any
+ Cons: - updating the UE position every 0.1 sec is a more aggressive approach
+
+ -------------------------------------------------------------------
+ '''
+
+ # flag = True
+
+ # while True:
+ # for point in points:
+
+ # #Iteration to find the last known coordinates of the UE
+ # #Then the movements begins from the last known position (geo coordinates)
+ # if ((UE.latitude != point["latitude"]) or (UE.longitude != point["longitude"])) and flag == True:
+ # continue
+ # elif (UE.latitude == point["latitude"]) and (UE.longitude == point["longitude"]) and flag == True:
+ # flag = False
+ # continue
+
+
+ # try:
+ # UE = crud.ue.update_coordinates(db=db, lat=point["latitude"], long=point["longitude"], db_obj=UE)
+ # cell_now = check_distance(UE.latitude, UE.longitude, json_cells) #calculate the distance from all the cells
+ # except Exception as ex:
+ # logging.warning("Failed to update coordinates")
+ # logging.warning(ex)
+
+ # if cell_now != None:
+ # if UE.Cell_id != cell_now.get('id'): #Cell has changed in the db "handover"
+ # logging.warning(f"UE({UE.supi}) with ipv4 {UE.ip_address_v4} handovers to Cell {cell_now.get('id')}, {cell_now.get('description')}")
+ # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : cell_now.get('id')})
+
+ # #Retrieve the subscription of the UE by external Id | This could be outside while true but then the user cannot subscribe when the loop runs
+ # # sub = crud.monitoring.get_sub_externalId(db=db, externalId=UE.external_identifier, owner_id=current_user.id)
+ # sub = crud_mongo.read(db_mongo, "MonitoringEvent", "externalId", UE.external_identifier)
+
+ # #Validation of subscription
+ # if not sub:
+ # logging.warning("Monitoring Event subscription not found")
+ # elif not crud.user.is_superuser(current_user) and (sub.get("owner_id") != current_user.id):
+ # logging.warning("Not enough permissions")
+ # else:
+ # sub_validate_time = tools.check_expiration_time(expire_time=sub.get("monitorExpireTime"))
+ # if sub_validate_time:
+ # sub = tools.check_numberOfReports(db_mongo, sub)
+ # if sub: #return the callback request only if subscription is valid
+ # try:
+ # response = location_callback(UE, sub.get("notificationDestination"), sub.get("link"))
+ # logging.info(response.json())
+ # except requests.exceptions.ConnectionError as ex:
+ # logging.warning("Failed to send the callback request")
+ # logging.warning(ex)
+ # crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id"))
+ # continue
+ # else:
+ # crud_mongo.delete_by_uuid(db_mongo, "MonitoringEvent", sub.get("_id"))
+ # logging.warning("Subscription has expired (expiration date)")
+
+ # #QoS Monitoring Event (handover)
+ # ues_connected = crud.ue.get_by_Cell(db=db, cell_id=UE.Cell_id)
+ # if len(ues_connected) > 1:
+ # gbr = 'QOS_NOT_GUARANTEED'
+ # else:
+ # gbr = 'QOS_GUARANTEED'
+
+ # logging.warning(gbr)
+ # qos_notification_control(gbr ,current_user, UE.ip_address_v4)
+ # logging.critical("Bypassed qos notification control")
+ # else:
+ # crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : None})
+
+ # # logging.info(f'User: {current_user.id} | UE: {supi} | Current location: latitude ={UE.latitude} | longitude = {UE.longitude} | Speed: {UE.speed}' )
+
+ # if UE.speed == 'LOW':
+ # time.sleep(1)
+ # elif UE.speed == 'HIGH':
+ # time.sleep(0.1)
+
+ # if self._stop_threads:
+ # print("Stop moving...")
+ # break
+
+ # if self._stop_threads:
+ # print("Terminating thread...")
+ # break
+ finally:
+ db.close()
+ return
+
+ def stop(self):
+ self._stop_threads = True
+
+#API
+router = APIRouter()
+
+@router.post("/start-loop", status_code=200)
+def initiate_movement(
+ *,
+ msg: Msg,
+ current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+ """
+ Start the loop.
+ """
+ if msg.supi in threads:
+ raise HTTPException(status_code=409, detail=f"There is a thread already running for this supi:{msg.supi}")
+ t = BackgroundTasks(args= (current_user, msg.supi, ))
+ threads[f"{msg.supi}"] = {}
+ threads[f"{msg.supi}"][f"{current_user.id}"] = t
+ t.start()
+ # print(threads)
+ return {"msg": "Loop started"}
+
+@router.post("/stop-loop", status_code=200)
+def terminate_movement(
+ *,
+ msg: Msg,
+ current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+ """
+ Stop the loop.
+ """
+ try:
+ threads[f"{msg.supi}"][f"{current_user.id}"].stop()
+ threads[f"{msg.supi}"][f"{current_user.id}"].join()
+ threads.pop(f"{msg.supi}")
+ return {"msg": "Loop ended"}
+ except KeyError as ke:
+ print('Key Not Found in Threads Dictionary:', ke)
+ raise HTTPException(status_code=409, detail="There is no thread running for this user! Please initiate a new thread")
+
+@router.get("/state-loop/{supi}", status_code=200)
+def state_movement(
+ *,
+ supi: str = Path(...),
+ current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+ """
+ Get the state
+ """
+ return {"running": retrieve_ue_state(supi, current_user.id)}
+
+@router.get("/state-ues", status_code=200)
+def state_ues(
+ current_user: models.User = Depends(deps.get_current_active_user),
+) -> Any:
+ """
+ Get the state
+ """
+ return ues
+
+#Functions
+def retrieve_ue_state(supi: str, user_id: int) -> bool:
+ try:
+ return threads[f"{supi}"][f"{user_id}"].is_alive()
+ except KeyError as ke:
+ print('Key Not Found in Threads Dictionary:', ke)
+ return False
+
+def retrieve_ues() -> dict:
+ return ues
+
+def retrieve_ue(supi: str) -> dict:
+ return ues.get(supi)
+
+
diff --git a/backend/app/app/api/api_v1/endpoints/utils.py b/backend/app/app/api/api_v1/endpoints/utils.py
index f88ae33f..70750b68 100644
--- a/backend/app/app/api/api_v1/endpoints/utils.py
+++ b/backend/app/app/api/api_v1/endpoints/utils.py
@@ -1,161 +1,19 @@
from datetime import datetime
-import threading, logging, time, requests, json
-from pymongo import MongoClient
+import logging, requests, json
from typing import Any
-from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
+from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import JSONResponse
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import HTTPException
from sqlalchemy.orm.session import Session
-from app.db.session import SessionLocal
from app import models, schemas, crud
from app.api import deps
from app.schemas import monitoringevent, UserPlaneNotificationData
-from app.tools.distance import check_distance
-from app.tools.send_callback import location_callback, qos_callback
-from app import tools
-from app.crud import crud_mongo
-from .qosInformation import qos_reference_match
from pydantic import BaseModel
from app.api.api_v1.endpoints.paths import get_random_point
+from app.api.api_v1.endpoints.ue_movement import retrieve_ue_state
-#Dictionary holding threads that are running per user id.
-threads = {}
-
-
-class BackgroundTasks(threading.Thread):
-
- def __init__(self, group=None, target=None, name=None, args=(), kwargs=None):
- super().__init__(group=group, target=target, name=name)
- self._args = args
- self._kwargs = kwargs
- self._stop_threads = False
- return
-
- def run(self):
-
- current_user = self._args[0]
- supi = self._args[1]
-
- try:
- db = SessionLocal()
-
- #Initiate UE - if exists
- UE = crud.ue.get_supi(db=db, supi=supi)
- if not UE:
- logging.warning("UE not found")
- threads.pop(f"{supi}")
- return
- if (UE.owner_id != current_user.id):
- logging.warning("Not enough permissions")
- threads.pop(f"{supi}")
- return
-
- #Retrieve paths & points
- path = crud.path.get(db=db, id=UE.path_id)
- if not path:
- logging.warning("Path not found")
- threads.pop(f"{supi}")
- return
- if (path.owner_id != current_user.id):
- logging.warning("Not enough permissions")
- threads.pop(f"{supi}")
- return
-
- points = crud.points.get_points(db=db, path_id=UE.path_id)
- points = jsonable_encoder(points)
-
- #Retrieve all the cells
- Cells = crud.cell.get_multi_by_owner(db=db, owner_id=current_user.id, skip=0, limit=100)
- json_cells = jsonable_encoder(Cells)
-
-
- flag = True
-
- while True:
- for point in points:
-
- #Iteration to find the last known coordinates of the UE
- #Then the movements begins from the last known position (geo coordinates)
- if ((UE.latitude != point["latitude"]) or (UE.longitude != point["longitude"])) and flag == True:
- continue
- elif (UE.latitude == point["latitude"]) and (UE.longitude == point["longitude"]) and flag == True:
- flag = False
- continue
-
-
- try:
- UE = crud.ue.update_coordinates(db=db, lat=point["latitude"], long=point["longitude"], db_obj=UE)
- cell_now = check_distance(UE.latitude, UE.longitude, json_cells) #calculate the distance from all the cells
- except Exception as ex:
- logging.warning("Failed to update coordinates")
- logging.warning(ex)
-
- if cell_now != None:
- if UE.Cell_id != cell_now.get('id'): #Cell has changed in the db "handover"
- logging.warning(f"UE({UE.supi}) with ipv4 {UE.ip_address_v4} handovers to Cell {cell_now.get('id')}, {cell_now.get('description')}")
- crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : cell_now.get('id')})
-
- #Retrieve the subscription of the UE by external Id | This could be outside while true but then the user cannot subscribe when the loop runs
- sub = crud.monitoring.get_sub_externalId(db=db, externalId=UE.external_identifier, owner_id=current_user.id)
-
- #Validation of subscription
- if not sub:
- logging.warning("Monitoring Event subscription not found")
- elif not crud.user.is_superuser(current_user) and (sub.owner_id != current_user.id):
- logging.warning("Not enough permissions")
- else:
- sub_validate_time = tools.check_expiration_time(expire_time=sub.monitorExpireTime)
- if sub_validate_time:
- sub = tools.check_numberOfReports(db=db, item_in=sub)
- if sub: #return the callback request only if subscription is valid
- try:
- response = location_callback(UE, sub.notificationDestination, sub.link)
- logging.info(response.json())
- except requests.exceptions.ConnectionError as ex:
- logging.warning("Failed to send the callback request")
- logging.warning(ex)
- crud.monitoring.remove(db=db, id=sub.id)
- continue
- else:
- crud.monitoring.remove(db=db, id=sub.id)
- logging.warning("Subscription has expired (expiration date)")
-
- #QoS Monitoring Event (handover)
- ues_connected = crud.ue.get_by_Cell(db=db, cell_id=UE.Cell_id)
- if len(ues_connected) > 1:
- gbr = 'QOS_NOT_GUARANTEED'
- else:
- gbr = 'QOS_GUARANTEED'
-
- logging.warning(gbr)
- qos_notification_control(gbr ,current_user, UE.ip_address_v4)
- logging.critical("Bypassed qos notification control")
- else:
- crud.ue.update(db=db, db_obj=UE, obj_in={"Cell_id" : None})
-
- # logging.info(f'User: {current_user.id} | UE: {supi} | Current location: latitude ={UE.latitude} | longitude = {UE.longitude} | Speed: {UE.speed}' )
-
- if UE.speed == 'LOW':
- time.sleep(1)
- elif UE.speed == 'HIGH':
- time.sleep(0.1)
-
- if self._stop_threads:
- print("Stop moving...")
- break
-
- if self._stop_threads:
- print("Terminating thread...")
- break
- finally:
- db.close()
- return
-
- def stop(self):
- self._stop_threads = True
-
-
+#List holding notifications from
event_notifications = []
counter = 0
@@ -199,50 +57,6 @@ def add_notifications(request: Request, response: JSONResponse, is_notification:
counter += 1
return json_data
-
-
-def qos_notification_control(gbr_status: str, current_user, ipv4):
- client = MongoClient("mongodb://mongo:27017", username='root', password='pass')
- db = client.fastapi
-
- doc = crud_mongo.read(db, 'QoSMonitoring', 'ipv4Addr', ipv4)
-
- #Check if the document exists
- if not doc:
- logging.warning("AsSessionWithQoS subscription not found")
- return
- #If the document exists then validate the owner
- if not crud.user.is_superuser(current_user) and (doc['owner_id'] != current_user.id):
- logging.info("Not enough permissions")
- return
-
- qos_standardized = qos_reference_match(doc.get('qosReference'))
-
- logging.critical(qos_standardized)
- logging.critical(qos_standardized.get('type'))
-
- logging.critical(doc.get('notificationDestination'))
- logging.critical(doc.get('link'))
-
- if qos_standardized.get('type') == 'GBR' or qos_standardized.get('type') == 'DC-GBR':
- try:
- logging.critical("Before response")
- response = qos_callback(doc.get('notificationDestination'), doc.get('link'), gbr_status, ipv4)
- logging.critical(f"Response from {doc.get('notificationDestination')}")
- except requests.exceptions.Timeout as ex:
- logging.critical("Failed to send the callback request")
- logging.critical(ex)
- except requests.exceptions.TooManyRedirects as ex:
- logging.critical("Failed to send the callback request")
- logging.critical(ex)
- except requests.exceptions.RequestException as ex:
- logging.critical("Failed to send the callback request")
- logging.critical(ex)
- else:
- logging.critical('Non-GBR subscription')
-
- return
-
router = APIRouter()
@@ -376,10 +190,6 @@ def create_scenario(
json_data['longitude'] = random_point.get('longitude')
crud.ue.update(db=db, db_obj=UE, obj_in=json_data)
-
-
-
-
if bool(err) == True:
raise HTTPException(status_code=409, detail=err)
@@ -469,58 +279,4 @@ def get_last_notifications(
break
skipped_items += 1
- return updated_notification
-
-@router.post("/start-loop", status_code=200)
-def initiate_movement(
- *,
- msg: schemas.Msg,
- current_user: models.User = Depends(deps.get_current_active_user),
-) -> Any:
- """
- Start the loop.
- """
- if msg.supi in threads:
- raise HTTPException(status_code=409, detail=f"There is a thread already running for this supi:{msg.supi}")
- t = BackgroundTasks(args= (current_user, msg.supi, ))
- threads[f"{msg.supi}"] = {}
- threads[f"{msg.supi}"][f"{current_user.id}"] = t
- t.start()
- print(threads)
- return {"msg": "Loop started"}
-
-@router.post("/stop-loop", status_code=200)
-def terminate_movement(
- *,
- msg: schemas.Msg,
- current_user: models.User = Depends(deps.get_current_active_user),
-) -> Any:
- """
- Stop the loop.
- """
- try:
- threads[f"{msg.supi}"][f"{current_user.id}"].stop()
- threads[f"{msg.supi}"][f"{current_user.id}"].join()
- threads.pop(f"{msg.supi}")
- return {"msg": "Loop ended"}
- except KeyError as ke:
- print('Key Not Found in Threads Dictionary:', ke)
- raise HTTPException(status_code=409, detail="There is no thread running for this user! Please initiate a new thread")
-
-@router.get("/state-loop/{supi}", status_code=200)
-def state_movement(
- *,
- supi: str = Path(...),
- current_user: models.User = Depends(deps.get_current_active_user),
-) -> Any:
- """
- Get the state
- """
- return {"running": retrieve_ue_state(supi, current_user.id)}
-
-def retrieve_ue_state(supi: str, user_id: int) -> bool:
- try:
- return threads[f"{supi}"][f"{user_id}"].is_alive()
- except KeyError as ke:
- print('Key Not Found in Threads Dictionary:', ke)
- return False
\ No newline at end of file
+ return updated_notification
\ No newline at end of file
diff --git a/backend/app/app/crud/crud_mongo.py b/backend/app/app/crud/crud_mongo.py
index ef5c84b9..c03377c5 100644
--- a/backend/app/app/crud/crud_mongo.py
+++ b/backend/app/app/crud/crud_mongo.py
@@ -18,6 +18,10 @@ def read(db : Database, collection_name : str, key : str, value):
collection = db[collection_name]
return collection.find_one({ key : value })
+def read_by_multiple_pairs(db : Database, collection_name : str, **kwargs):
+ collection = db[collection_name]
+ return collection.find_one({ **kwargs })
+
# PUT
def update(db: Database, collection_name, uuId, json_data):
return db[collection_name].replace_one({"_id": ObjectId(uuId)}, json_data)
@@ -31,10 +35,14 @@ def create(db: Database, collection_name, json_data):
return db[collection_name].insert_one(json_data)
# DELETE
-def delete(db: Database, collection_name, uuId):
+def delete_by_uuid(db: Database, collection_name, uuId):
result = db[collection_name].delete_one({"_id": ObjectId(uuId)})
return result
+def delete_by_item(db: Database, collection_name, key: str, value):
+ result = db[collection_name].delete_one({key: value})
+ return result
+
#Read all profiles by gNB id (QoSProfile)
def read_all_gNB_profiles(db : Database, collection_name, id):
diff --git a/backend/app/app/db/session.py b/backend/app/app/db/session.py
index 6e281526..1e6b7f18 100644
--- a/backend/app/app/db/session.py
+++ b/backend/app/app/db/session.py
@@ -3,5 +3,5 @@
from app.core.config import settings
-engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True) #Create a db URL for SQLAlchemy in core/config.py/ Settings class
+engine = create_engine(settings.SQLALCHEMY_DATABASE_URI, pool_pre_ping=True, pool_size=150, max_overflow=20) #Create a db URL for SQLAlchemy in core/config.py/ Settings class
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) #Each instance is a db session
diff --git a/backend/app/app/main.py b/backend/app/app/main.py
index 1657b26f..09fe7dfa 100644
--- a/backend/app/app/main.py
+++ b/backend/app/app/main.py
@@ -2,6 +2,7 @@
from starlette.middleware.cors import CORSMiddleware
from app.api.api_v1.api import api_router, nef_router
from app.core.config import settings
+import time
# imports for UI
from fastapi.staticfiles import StaticFiles
@@ -32,6 +33,14 @@
nefapi.include_router(nef_router, prefix=settings.API_V1_STR)
app.mount("/nef", nefapi)
+#Middleware - add a custom header X-Process-Time containing the time in seconds that it took to process the request and generate a response
+@app.middleware("http")
+async def add_process_time_header(request: Request, call_next):
+ start_time = time.time()
+ response = await call_next(request)
+ process_time = time.time() - start_time
+ response.headers["X-Process-Time"] = str(process_time)
+ return response
# ================================= Static Page routes =================================
diff --git a/backend/app/app/schemas/monitoringevent.py b/backend/app/app/schemas/monitoringevent.py
index 6529554a..1f844862 100644
--- a/backend/app/app/schemas/monitoringevent.py
+++ b/backend/app/app/schemas/monitoringevent.py
@@ -28,7 +28,8 @@ class MonitoringEventReport(BaseModel):
externalId: Optional[str] = Field("123456789@domain.com", description="Globally unique identifier containing a Domain Identifier and a Local Identifier. \@\")
monitoringType: MonitoringType
locationInfo: Optional[LocationInfo] = None
- ipv4Addr: Optional[IPvAnyAddress] = Field(None, description="String identifying an Ipv4 address")
+ ipv4Addr: Optional[IPvAnyAddress] = Field(None, description="String identifying an Ipv4 address")
+
class MonitoringEventSubscriptionCreate(BaseModel):
# mtcProviderId: Optional[str] = Field(None, description="Identifies the MTC Service Provider and/or MTC Application")
@@ -43,6 +44,7 @@ class MonitoringEventSubscriptionCreate(BaseModel):
monitoringType: MonitoringType
maximumNumberOfReports: Optional[int] = Field(None, description="Identifies the maximum number of event reports to be generated. Value 1 makes the Monitoring Request a One-time Request", ge=1)
monitorExpireTime: Optional[datetime] = Field(None, description="Identifies the absolute time at which the related monitoring event request is considered to expire")
+ maximumDetectionTime: Optional[int] = Field(None, description="If monitoringType is \"LOSS_OF_CONNECTIVITY\", this parameter may be included to identify the maximum period of time after which the UE is considered to be unreachable.", gt=0)
# monitoringEventReport: Optional[MonitoringEventReport] = None
class MonitoringEventSubscription(MonitoringEventSubscriptionCreate):
@@ -53,6 +55,7 @@ class Config:
class MonitoringNotification(MonitoringEventReport):
subscription: AnyHttpUrl
+ lossOfConnectReason: Optional[int] = Field(None, description= "According to 3GPP TS 29.522 the lossOfConnectReason attribute shall be set to 6 if the UE is deregistered, 7 if the maximum detection timer expires or 8 if the UE is purged")
class MonitoringEventReportReceived(BaseModel):
ok: bool
\ No newline at end of file
diff --git a/backend/app/app/static/NEF_logo_400x160.svg b/backend/app/app/static/NEF_logo_400x160.svg
new file mode 100644
index 00000000..415eb6e5
--- /dev/null
+++ b/backend/app/app/static/NEF_logo_400x160.svg
@@ -0,0 +1,41 @@
+
+
+
+
diff --git a/backend/app/app/static/NEF_logo_400x160_light.svg b/backend/app/app/static/NEF_logo_400x160_light.svg
new file mode 100644
index 00000000..5d1bcdfa
--- /dev/null
+++ b/backend/app/app/static/NEF_logo_400x160_light.svg
@@ -0,0 +1,43 @@
+
+
+
+
diff --git a/backend/app/app/static/NEF_logo_400x400.svg b/backend/app/app/static/NEF_logo_400x400.svg
new file mode 100644
index 00000000..cc07ef9a
--- /dev/null
+++ b/backend/app/app/static/NEF_logo_400x400.svg
@@ -0,0 +1,42 @@
+
+
+
+
diff --git a/backend/app/app/static/NEF_logo_400x400_light.svg b/backend/app/app/static/NEF_logo_400x400_light.svg
new file mode 100644
index 00000000..1e018555
--- /dev/null
+++ b/backend/app/app/static/NEF_logo_400x400_light.svg
@@ -0,0 +1,41 @@
+
+
+
+
diff --git a/backend/app/app/static/app/app.js b/backend/app/app/static/app/app.js
index 67caeb18..74bb0dae 100644
--- a/backend/app/app/static/app/app.js
+++ b/backend/app/app/static/app/app.js
@@ -13,33 +13,38 @@
var app = {
local_storage_available: false,
auth_obj: null,
- api_url: "/api/v1"
+ api_url: "/api/v1",
+ default_redirect: "/dashboard"
};
// ======================================================
+// check if local storage is available &
+// update `local_storage_available` variable
+app_test_browser_local_storage();
+// initialize auth_obj
+if (app.local_storage_available) {
+ app.auth_obj = JSON.parse(localStorage.getItem('app_auth'));
-$( document ).ready(function() {
-
- // check if local storage is available &
- // update `local_storage_available` variable
- browser_test_local_storage();
-
- // initialize auth_obj
- if (app.local_storage_available) {
- app.auth_obj = JSON.parse(localStorage.getItem('app_auth'));
-
- if ( app.auth_obj == null ) {
- // if you can't find a token redirect to login page
- window.location.href = [location.protocol, '//', location.host, "/login"].join('');
- } else {
- // use the API to test the token found
- // to check that it is valid
- api_test_token( app.auth_obj.access_token );
+ if ( app.auth_obj == null ) {
+ // if you the token is null
+ // check if you are already at the /login page
+ if (location.pathname != "/login") {
+ // if not, redirect to /err401
+ window.location.href = [location.protocol, '//', location.host, "/err401"].join('');
}
+
+ } else {
+ // use the API to test the token found
+ // to check that it is valid
+ api_test_token( app.auth_obj.access_token );
}
+}
+
+
+$( document ).ready(function() {
ui_initialize_btn_listeners();
@@ -49,9 +54,9 @@ $( document ).ready(function() {
-// ================== Functions Area ==================
+// ================= App Functions Area =================
//
-function browser_test_local_storage(){
+function app_test_browser_local_storage(){
var test = 'test';
try {
localStorage.setItem(test, test);
@@ -64,6 +69,17 @@ function browser_test_local_storage(){
+
+function app_login( token_data ) {
+ // console.log(data);
+ if (app.local_storage_available) {
+ localStorage.setItem('app_auth', JSON.stringify(token_data));
+ }
+}
+
+
+
+
// ================== Ajax Calls Area ==================
//
@@ -80,11 +96,59 @@ function api_test_token( token_str ){
processData: false,
success: function(data)
{
- //
+ if (location.pathname == "/login") {
+ window.location.href = [location.protocol, '//', location.host, app.default_redirect].join('');
+ }
},
error: function(err)
{
- window.location.href = [location.protocol, '//', location.host, "/login"].join('');
+ if (location.pathname == "/login") {
+ // $('.login-notifications .text-danger').text("Session has expired, login again.");
+ ui_show_login_error(err);
+ return;
+ }
+ window.location.href = [location.protocol, '//', location.host, "/err401"].join('');
+ },
+ timeout: 5000
+ });
+}
+
+
+
+
+function api_login_access_token(user , pass) {
+ var url = app.api_url + '/login/access-token';
+ var data = {
+ "grant_type" : "",
+ "username" : user,
+ "password" : pass,
+ "scope" : "",
+ "client_id" : "",
+ "client_secret":""
+ };
+
+ $.ajax({
+ type: 'POST',
+ url: url,
+ contentType : 'application/x-www-form-urlencoded; charset=UTF-8',
+ data: data,
+ processData: true,
+ beforeSend: function() {
+ $('.spinner-grow-sm').show();
+ $('.login-notifications .text-secondary').text("Checking user authentication...");
+ },
+ success: function(data)
+ {
+ app_login(data);
+ ui_show_login_success();
+ },
+ error: function(err)
+ {
+ ui_show_login_error(err);
+ },
+ complete: function()
+ {
+ $('.spinner-grow-sm').hide();
},
timeout: 5000
});
@@ -97,6 +161,6 @@ function ui_initialize_btn_listeners() {
$('#logout-btn').on("click",function(event){
event.preventDefault();
localStorage.removeItem('app_auth');
- window.location.href = [location.protocol, '//', location.host].join('');
+ window.location.href = [location.protocol, '//', location.host, "/login"].join('');
});
}
\ No newline at end of file
diff --git a/backend/app/app/static/app/login.js b/backend/app/app/static/app/login.js
index 9c23547e..9e6cf523 100644
--- a/backend/app/app/static/app/login.js
+++ b/backend/app/app/static/app/login.js
@@ -1,77 +1,29 @@
-// ================== Global variables ==================
-
-var api_url = "/api/v1"
-var username = "";
-var password = "";
-var local_storage_available = false;
-
-// ======================================================
-
-
-
$( document ).ready(function() {
- // check if local storage is available
- // updates `local_storage_available` variable
- browser_test_local_storage();
-
-
+ // submit when button is clicked...
$('#btn-login').on('click',function(){
- username = $('#input-user').val();
- password = $('#input-pass').val();
-
- api_login_access_token( username , password );
+ ui_submit_login();
});
-});
-
-function api_login_access_token(user , pass) {
- var url = api_url + '/login/access-token';
- var data = {
- "grant_type" : "",
- "username" : user,
- "password" : pass,
- "scope" : "",
- "client_id" : "",
- "client_secret":""
- };
- $.ajax({
- type: 'POST',
- url: url,
- contentType : 'application/x-www-form-urlencoded; charset=UTF-8',
- data: data,
- processData: true,
- beforeSend: function() {
- $('.spinner-grow-sm').show();
- $('.login-notifications .text-secondary').text("Checking user authentication...");
- },
- success: function(data)
- {
- app_login(data);
- ui_show_login_success();
- },
- error: function(err)
- {
- ui_show_login_error(err);
- },
- complete: function()
- {
- $('.spinner-grow-sm').hide();
- },
- timeout: 5000
+ // also submit when the user just hits "enter"
+ $(".card-body input").keypress(function(event) {
+ if(event && event.keyCode == 13) {
+ ui_submit_login();
+ }
});
-}
+});
+
+function ui_submit_login() {
+ console.log("login pressed");
+ username = $('#input-user').val();
+ password = $('#input-pass').val();
-function app_login( token_data ) {
- // console.log(data);
- if (local_storage_available) {
- localStorage.setItem('app_auth', JSON.stringify(token_data));
- }
+ api_login_access_token( username , password );
}
@@ -80,23 +32,11 @@ function ui_show_login_error(err) {
$('.login-notifications .text-danger').text(err.responseJSON.detail);
}
+
+
function ui_show_login_success() {
$('.login-notifications .text-secondary').text("Successful login, redirecting...");
setInterval(function(){
window.location.href = [location.protocol, '//', location.host, "/dashboard"].join('');
},1200);
}
-
-
-
-
-function browser_test_local_storage(){
- var test = 'test';
- try {
- localStorage.setItem(test, test);
- localStorage.removeItem(test);
- local_storage_available = true;
- } catch(e) {
- local_storage_available = false;
- }
-}
\ No newline at end of file
diff --git a/backend/app/app/static/js/dashboard-cells.js b/backend/app/app/static/js/dashboard-cells.js
index 8b4e9df2..645aa349 100644
--- a/backend/app/app/static/js/dashboard-cells.js
+++ b/backend/app/app/static/js/dashboard-cells.js
@@ -178,7 +178,7 @@ function api_put_cell( cell_obj ) {
error: function(err)
{
console.log(err);
- ui_display_toast_msg("error", "Error: Cell could not be updated", err.responseJSON.detail[0].msg);
+ ui_display_toast_msg("error", "Error: Cell could not be updated", err.responseJSON.detail);
},
complete: function()
{
diff --git a/backend/app/app/static/js/dashboard-ues.js b/backend/app/app/static/js/dashboard-ues.js
index c57ce851..3feee311 100644
--- a/backend/app/app/static/js/dashboard-ues.js
+++ b/backend/app/app/static/js/dashboard-ues.js
@@ -242,7 +242,7 @@ function api_post_UE_callback( UE_obj, callback ) {
//
//
function api_get_state_loop_for( UE_supi ) {
- var url = app.api_url + '/utils/state-loop/' + UE_supi;
+ var url = app.api_url + '/ue_movement/state-loop/' + UE_supi;
$.ajax({
type: 'GET',
@@ -454,8 +454,8 @@ function ui_add_btn_listeners_for_UEs_CUD_operations() {
// api calls
api_put_UE_callback( edit_UE_tmp_obj, function(UE_obj){
- // on success, assign path (if selected)
- api_post_assign_path( UE_obj.supi, assign_path_id );
+ // // on success, assign path (if selected)
+ // api_post_assign_path( UE_obj.supi, assign_path_id );
ui_fetch_and_update_ues_data();
});
});
@@ -695,6 +695,8 @@ function ui_edit_UE_modal_add_listeners() {
edit_UE_selected_path_lg .clearLayers();
ui_map_paint_path(data, edit_UE_map, edit_UE_selected_path_lg, 0.75);
});
+
+ api_post_assign_path( edit_UE_tmp_obj.supi, selected_path_id );
}
else {
edit_UE_selected_path_lg.clearLayers();
diff --git a/backend/app/app/static/js/map.js b/backend/app/app/static/js/map.js
index 7c317330..b590db22 100644
--- a/backend/app/app/static/js/map.js
+++ b/backend/app/app/static/js/map.js
@@ -10,6 +10,8 @@ var cells = null;
var ues = null;
var paths = null;
+var moving_ues = null;
+
// variables used for painting / updating the map
// map layer groups
@@ -21,8 +23,7 @@ var cells_lg = L.layerGroup(),
var ue_markers = {};
var cell_markers = {};
var map_bounds = [];
-// helper var for correct initialization
-var UEs_first_paint = true;
+
// for UE & map refresh
var UE_refresh_interval = null;
@@ -30,7 +31,7 @@ var UE_refresh_sec_default = 1000; // 1 sec
var UE_refresh_sec = -1; // when select = "off" AND disabled = true
// template for UE buttons
-var ue_btn_tpl = ` `
+var ue_btn_tpl = ` `
var looping_UEs = 0;
@@ -86,14 +87,20 @@ $( document ).ready(function() {
// 1. get and paint every path per UE
// 2. create start/stop buttons
for (const ue of ues) {
+
+ // if no path selected, skip map paint and creation of button
+ if (ue.path_id == 0) { continue; }
+
// if not already fetched and painted, do so
if ( !helper_check_path_is_already_painted( ue.path_id ) ) {
api_get_specific_path(ue.path_id);
- paths_painted.push(ue.path_id);
+ paths_painted[ue.path_id] = true;
}
ui_generate_loop_btn_for( ue );
ui_set_loop_btn_status_for( ue );
}
+
+
if ( ues.length >0 ) {
ui_add_ue_btn_listeners();
ui_add_ue_all_btn_listener();
@@ -101,6 +108,11 @@ $( document ).ready(function() {
else {
$('#btn-start-all').removeClass("btn-success").addClass("btn-secondary").attr("disabled",true);
}
+
+ // edge case: UEs with no paths assigned --> disable button
+ if (paths_painted.length == 0) {
+ $('#btn-start-all').removeClass("btn-success").addClass("btn-secondary").attr("disabled",true);
+ }
}
}, 100);
};
@@ -155,7 +167,7 @@ function start_map_refresh_interval() {
// start updating
UE_refresh_interval = setInterval(function(){
- api_get_UEs();
+ api_get_moving_UEs();
}, UE_refresh_sec);
// enable the select button
@@ -300,7 +312,7 @@ function ui_initialize_map() {
//
function api_get_UEs() {
- var url = app.api_url + '/UEs?skip=0&limit=100';
+ var url = app.api_url + '/UEs?skip=0&limit=1000';
$.ajax({
type: 'GET',
@@ -327,71 +339,139 @@ function api_get_UEs() {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
-// 1. At first Ajax call, UE marks are generated and painted on the map
-// 2. At later Ajax calls, the marks are just updated (coordinates and popup content)
+
+// Ajax request to get UEs data
+// on success: paint the UE marks on the map
+//
+function api_get_moving_UEs() {
+
+ var url = app.api_url + '/ue_movement/state-ues';
+
+ $.ajax({
+ type: 'GET',
+ url: url,
+ contentType : 'application/json',
+ headers: {
+ "authorization": "Bearer " + app.auth_obj.access_token
+ },
+ processData: false,
+ beforeSend: function() {
+ //
+ },
+ success: function(data)
+ {
+ // console.log(data);
+ moving_ues = data;
+ ui_map_paint_moving_UEs();
+ },
+ error: function(err)
+ {
+ console.log(err);
+ },
+ complete: function()
+ {
+ //
+ },
+ timeout: 60000
+ });
+}
+
+
+
+// Function used after api_get_UEs() is called.
+// All the UE marks are generated and painted on the map (both moving & stationary)
+// At later Ajax calls, only the moving UEs are fetched and re-painted (check the following function)
//
function ui_map_paint_UEs() {
+ // console.log(ues);
+
for (const ue of ues) {
- if (UEs_first_paint) {
- // create markers - this will be executed only once!
- var walk_icon = L.divIcon({
- className: 'emu-pin-box',
- iconSize: L.point(30,42),
- iconAnchor: L.point(15,42),
- popupAnchor: L.point(0,-38),
- tooltipAnchor: L.point(0,0),
- html: '\
- '
- });
-
- ue_markers[ue.supi] = L.marker([ue.latitude,ue.longitude], {icon: walk_icon}).addTo(mymap)
- .bindTooltip(ue.ip_address_v4)
- .bindPopup(""+ ue.name +" "+
- // ue.description +" "+
- "location: [" + ue.latitude.toFixed(6) + "," + ue.longitude.toFixed(6) +"] "+
- "Cell ID: " + ( (ue.cell_id_hex==null)? "-" : ue.cell_id_hex ) +" "+
- "External identifier: " + ue.external_identifier +" "+
- "Speed:"+ ue.speed)
- .addTo(ues_lg); // add to layer group
+ // create markers - this will be executed only once!
+ var walk_icon = L.divIcon({
+ className: 'emu-pin-box',
+ iconSize: L.point(30,42),
+ iconAnchor: L.point(15,42),
+ popupAnchor: L.point(0,-38),
+ tooltipAnchor: L.point(0,0),
+ html: '\
+ '
+ });
+
+ ue_markers[ue.supi] = L.marker([ue.latitude,ue.longitude], {icon: walk_icon}).addTo(mymap)
+ .bindTooltip(ue.ip_address_v4)
+ .bindPopup(""+ ue.name +" "+
+ // ue.description +" "+
+ "location: [" + ue.latitude.toFixed(6) + "," + ue.longitude.toFixed(6) +"] "+
+ "Cell ID: " + ( (ue.cell_id_hex==null)? "-" : ue.cell_id_hex ) +" "+
+ "External identifier: " + ue.external_identifier +" "+
+ "Speed:"+ ue.speed)
+ .addTo(ues_lg); // add to layer group
+
+ if ( ue.cell_id_hex==null ) {
+ L.DomUtil.addClass(ue_markers[ue.supi]._icon, 'null-cell');
+ } else {
+ L.DomUtil.removeClass(ue_markers[ue.supi]._icon, 'null-cell');
+ }
+ }
+}
+
+
- if ( ue.cell_id_hex==null ) {
- L.DomUtil.addClass(ue_markers[ue.supi]._icon, 'null-cell');
- } else {
- L.DomUtil.removeClass(ue_markers[ue.supi]._icon, 'null-cell');
- }
- }
- else {
- // move existing markers
- var newLatLng = [ue.latitude,ue.longitude];
- ue_markers[ue.supi].setLatLng(newLatLng);
- ue_markers[ue.supi].setPopupContent(""+ ue.name +" "+
- // ue.description +" "+
- "location: [" + ue.latitude.toFixed(6) + "," + ue.longitude.toFixed(6) +"] "+
- "Cell ID: " + ( (ue.cell_id_hex==null)? "-" : ue.cell_id_hex ) +" "+
- "External identifier: " + ue.external_identifier +" "+
- "Speed:"+ ue.speed);
+// Function used after api_get_moving_UEs() is called.
+// It re-paints those marks (UEs) that are currently moving.
+//
+function ui_map_paint_moving_UEs() {
+
+ // moving_ues is returned from the backend as a key-value dict
+
+ for(var key in moving_ues) {
+
+ var ue = moving_ues[key];
+
+ // move existing markers
+ var newLatLng = [ue.latitude,ue.longitude];
+ ue_markers[ue.supi].setLatLng(newLatLng);
+ ue_markers[ue.supi].setPopupContent(""+ ue.name +" "+
+ // ue.description +" "+
+ "location: [" + ue.latitude.toFixed(6) + "," + ue.longitude.toFixed(6) +"] "+
+ "Cell ID: " + ( (ue.cell_id_hex==null)? "-" : ue.cell_id_hex ) +" "+
+ "External identifier: " + ue.external_identifier +" "+
+ "Speed:"+ ue.speed);
+
+
+ // update UE marker color
+ temp_icon = L.DomUtil.get(ue_markers[ue.supi]._icon);
+
+ if (temp_icon == null) {
+ // if the user has unchecked the UEs checkbox ✅ on the map settings
+ // temp_icon is null and triggers console errors
+ // if this is the case, continue...
+ continue;
+ } else {
if ( ue.cell_id_hex==null ) {
- L.DomUtil.addClass(ue_markers[ue.supi]._icon, 'null-cell');
+ // 'null-cell' class gives a grey color
+ // to UEs that are not connected to a cell
+ L.DomUtil.addClass(temp_icon, 'null-cell');
} else {
- L.DomUtil.removeClass(ue_markers[ue.supi]._icon, 'null-cell');
+ L.DomUtil.removeClass(temp_icon, 'null-cell');
}
}
}
- UEs_first_paint = false;
}
+
function api_get_Cells() {
var url = app.api_url + '/Cells/?skip=0&limit=100';
@@ -421,7 +501,7 @@ function api_get_Cells() {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -509,7 +589,7 @@ function api_get_specific_path( id ) {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -557,7 +637,7 @@ function fix_points_format( datapoints ) {
//
function api_start_loop( ue ) {
- var url = app.api_url + '/utils/start-loop';
+ var url = app.api_url + '/ue_movement/start-loop';
var data = {
"supi": ue.supi
};
@@ -577,8 +657,8 @@ function api_start_loop( ue ) {
success: function(data)
{
// console.log(data);
- $("#btn-ue-"+ue.id).data("running",true);
- $("#btn-ue-"+ue.id).removeClass('btn-success').addClass('btn-danger');
+ $("#btn-ue-"+ue.supi).data("running",true);
+ $("#btn-ue-"+ue.supi).removeClass('btn-success').addClass('btn-danger');
looping_UEs++;
if (looping_UEs == ues.length) {
@@ -594,7 +674,7 @@ function api_start_loop( ue ) {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -606,7 +686,7 @@ function api_start_loop( ue ) {
//
function api_stop_loop( ue ) {
- var url = app.api_url + '/utils/stop-loop';
+ var url = app.api_url + '/ue_movement/stop-loop';
var data = {
"supi": ue.supi
};
@@ -626,8 +706,8 @@ function api_stop_loop( ue ) {
success: function(data)
{
// console.log(data);
- $("#btn-ue-"+ue.id).data("running",false);
- $("#btn-ue-"+ue.id).addClass('btn-success').removeClass('btn-danger');
+ $("#btn-ue-"+ue.supi).data("running",false);
+ $("#btn-ue-"+ue.supi).addClass('btn-success').removeClass('btn-danger');
looping_UEs--;
if (looping_UEs == 0) {
@@ -644,7 +724,7 @@ function api_stop_loop( ue ) {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -657,7 +737,7 @@ function api_stop_loop( ue ) {
// and adds it to the ue-btn-area
//
function ui_generate_loop_btn_for( ue ) {
- var html_str = ue_btn_tpl.replaceAll("{{id}}", ue.id).replace("{{name}}",ue.name).replace("{{supi}}",ue.supi);
+ var html_str = ue_btn_tpl.replaceAll("{{supi}}", ue.supi).replaceAll("{{name}}",ue.name);
$(".ue-btn-area").append(html_str);
}
@@ -671,7 +751,7 @@ function ui_generate_loop_btn_for( ue ) {
// It also updates the start-all/stop-all button in case all the UEs are moving
//
function ui_set_loop_btn_status_for(ue) {
- var url = app.api_url + '/utils/state-loop/' + ue.supi;
+ var url = app.api_url + '/ue_movement/state-loop/' + ue.supi;
$.ajax({
type: 'GET',
@@ -689,8 +769,8 @@ function ui_set_loop_btn_status_for(ue) {
{
// console.log(data);
if ( data.running ) {
- $('#btn-ue-'+ue.id).removeClass('btn-success').addClass('btn-danger');
- $('#btn-ue-'+ue.id).data("running",data.running);
+ $('#btn-ue-'+ue.supi).removeClass('btn-success').addClass('btn-danger');
+ $('#btn-ue-'+ue.supi).data("running",data.running);
looping_UEs++;
if (looping_UEs == ues.length) {
@@ -709,7 +789,7 @@ function ui_set_loop_btn_status_for(ue) {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -855,7 +935,7 @@ function api_get_all_monitoring_events() {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -899,7 +979,7 @@ function api_get_last_monitoring_events() {
{
//
},
- timeout: 5000
+ timeout: 60000
});
}
@@ -914,7 +994,7 @@ function ui_init_datatable_events() {
paging: false,
searching: true,
info: false,
- order: [[4, 'desc']],
+ order: [[5, 'desc']],
pageLength: -1,
lengthMenu: [[10, 25, 50, -1], [10, 25, 50, "All"]],
columnDefs: [
@@ -925,8 +1005,24 @@ function ui_init_datatable_events() {
"orderable" : true,
"searchable": true,
},
+ // {
+ // "targets": 1,
+ // "data": null,
+ // "visible": true,
+ // "orderable": true,
+ // "searchable": true,
+ // "render": function ( data, type, row ) {
+ // details = helper_get_event_details( row.id );
+
+ // if (details.request_body != null) {
+ // return JSON.parse(details.request_body).monitoringType;
+ // } else {
+ // return "-";
+ // }
+ // }
+ // },
{
- "targets": 5,
+ "targets": 6,
"data": null,
"defaultContent": '',
"orderable" : false,
@@ -939,6 +1035,7 @@ function ui_init_datatable_events() {
],
columns: [
{ "data": "id", className: "dt-center" },
+ { "data": "serviceAPI" },
{ "data": "isNotification",
"render": function(data) {
if (data) {
@@ -985,6 +1082,7 @@ function ui_append_datatable_events(data) {
events_datatbl.rows.add( [{
id: event.id,
+ serviceAPI: event.serviceAPI,
isNotification: event.isNotification,
method: event.method,
status_code: event.status_code,
@@ -1002,7 +1100,7 @@ function ui_append_datatable_events(data) {
function show_details_modal( event_id ) {
- details = get_event_details( event_id );
+ details = helper_get_event_details( event_id );
// load event details
$("#modal_srv").html( details.serviceAPI );
@@ -1024,7 +1122,7 @@ function show_details_modal( event_id ) {
}
-function get_event_details( event_id ) {
+function helper_get_event_details( event_id ) {
for (const event of events) {
if (event.id == event_id) return event;
}
@@ -1033,10 +1131,8 @@ function get_event_details( event_id ) {
function helper_check_path_is_already_painted( path_id ) {
- for (const item of paths_painted) {
- if ( item == path_id ) {
- return true
- }
+ if ( paths_painted[ path_id ] != true) {
+ return false;
}
- return false;
+ return true;
}
\ No newline at end of file
diff --git a/backend/app/app/static/js/register.js b/backend/app/app/static/js/register.js
index 83fc06ab..d6a04e94 100644
--- a/backend/app/app/static/js/register.js
+++ b/backend/app/app/static/js/register.js
@@ -13,36 +13,17 @@ var email = "";
$( document ).ready(function() {
+ // submit when button is clicked...
$('#btn-register').on('click',function(){
- username = $('#input-user').val();
- email = $('#input-email').val();
- password = $('#input-pass').val();
- password2 = $('#input-pass-repeat').val();
-
- // remove previous notifications when button is clicked
- $('.register-notifications .text-secondary').text("");
- $('.register-notifications .text-danger').text("");
-
- // UI pre-checks
- if (
- (username == "") ||
- (email == "") ||
- (password == "") ||
- (password2== ""))
- {
- ui_show_precheck_error("Some input fields are empty.");
- return;
- }
- if (password != password2) {
- ui_show_precheck_error("Oups! The two passwords you typed don't match.");
- return;
- }
- if (!validateEmail( email )) {
- ui_show_precheck_error("Psst! The email you typed is not valid.");
- return;
- }
+ ui_submit_register();
+ });
+
- api_create_user_open();
+ // also submit when the user just hits "enter"
+ $(".card-body input").keypress(function(event) {
+ if(event && event.keyCode == 13) {
+ ui_submit_register();
+ }
});
});
@@ -83,6 +64,40 @@ function api_create_user_open() {
+function ui_submit_register() {
+ username = $('#input-user').val();
+ email = $('#input-email').val();
+ password = $('#input-pass').val();
+ password2 = $('#input-pass-repeat').val();
+
+ // remove previous notifications when button is clicked
+ $('.register-notifications .text-secondary').text("");
+ $('.register-notifications .text-danger').text("");
+
+ // UI pre-checks
+ if (
+ (username == "") ||
+ (email == "") ||
+ (password == "") ||
+ (password2== ""))
+ {
+ ui_show_precheck_error("Some input fields are empty.");
+ return;
+ }
+ if (password != password2) {
+ ui_show_precheck_error("Oups! The two passwords you typed don't match.");
+ return;
+ }
+ if (!validateEmail( email )) {
+ ui_show_precheck_error("Psst! The email you typed is not valid.");
+ return;
+ }
+
+ api_create_user_open();
+}
+
+
+
function ui_show_precheck_error( info ) {
$('.register-notifications .text-danger').text( info );
}
diff --git a/backend/app/app/static/leaflet@1.7.1/leaflet-src.esm.js.map b/backend/app/app/static/leaflet@1.7.1/leaflet-src.esm.js.map
deleted file mode 100644
index 0d090781..00000000
--- a/backend/app/app/static/leaflet@1.7.1/leaflet-src.esm.js.map
+++ /dev/null
@@ -1 +0,0 @@
-{"version":3,"file":"leaflet-src.esm.js","sources":["../src/core/Util.js","../src/core/Class.js","../src/core/Events.js","../src/geometry/Point.js","../src/geometry/Bounds.js","../src/geo/LatLngBounds.js","../src/geo/LatLng.js","../src/geo/crs/CRS.js","../src/geo/crs/CRS.Earth.js","../src/geo/projection/Projection.SphericalMercator.js","../src/geometry/Transformation.js","../src/geo/crs/CRS.EPSG3857.js","../src/layer/vector/SVG.Util.js","../src/core/Browser.js","../src/dom/DomEvent.Pointer.js","../src/dom/DomEvent.DoubleTap.js","../src/dom/DomUtil.js","../src/dom/DomEvent.js","../src/dom/PosAnimation.js","../src/map/Map.js","../src/control/Control.js","../src/control/Control.Layers.js","../src/control/Control.Zoom.js","../src/control/Control.Scale.js","../src/control/Control.Attribution.js","../src/control/index.js","../src/core/Handler.js","../src/core/index.js","../src/dom/Draggable.js","../src/geometry/LineUtil.js","../src/geometry/PolyUtil.js","../src/geo/projection/Projection.LonLat.js","../src/geo/projection/Projection.Mercator.js","../src/geo/projection/index.js","../src/geo/crs/CRS.EPSG3395.js","../src/geo/crs/CRS.EPSG4326.js","../src/geo/crs/CRS.Simple.js","../src/geo/crs/index.js","../src/layer/Layer.js","../src/layer/LayerGroup.js","../src/layer/FeatureGroup.js","../src/layer/marker/Icon.js","../src/layer/marker/Icon.Default.js","../src/layer/marker/Marker.Drag.js","../src/layer/marker/Marker.js","../src/layer/vector/Path.js","../src/layer/vector/CircleMarker.js","../src/layer/vector/Circle.js","../src/layer/vector/Polyline.js","../src/layer/vector/Polygon.js","../src/layer/GeoJSON.js","../src/layer/ImageOverlay.js","../src/layer/VideoOverlay.js","../src/layer/SVGOverlay.js","../src/layer/DivOverlay.js","../src/layer/Popup.js","../src/layer/Tooltip.js","../src/layer/marker/DivIcon.js","../src/layer/marker/index.js","../src/layer/tile/GridLayer.js","../src/layer/tile/TileLayer.js","../src/layer/tile/TileLayer.WMS.js","../src/layer/tile/index.js","../src/layer/vector/Renderer.js","../src/layer/vector/Canvas.js","../src/layer/vector/SVG.VML.js","../src/layer/vector/SVG.js","../src/layer/vector/Renderer.getRenderer.js","../src/layer/vector/Rectangle.js","../src/layer/vector/index.js","../src/layer/index.js","../src/map/handler/Map.BoxZoom.js","../src/map/handler/Map.DoubleClickZoom.js","../src/map/handler/Map.Drag.js","../src/map/handler/Map.Keyboard.js","../src/map/handler/Map.ScrollWheelZoom.js","../src/map/handler/Map.Tap.js","../src/map/handler/Map.TouchZoom.js","../src/map/index.js"],"sourcesContent":["/*\r\n * @namespace Util\r\n *\r\n * Various utility functions, used by Leaflet internally.\r\n */\r\n\r\n// @function extend(dest: Object, src?: Object): Object\r\n// Merges the properties of the `src` object (or multiple objects) into `dest` object and returns the latter. Has an `L.extend` shortcut.\r\nexport function extend(dest) {\r\n\tvar i, j, len, src;\r\n\r\n\tfor (j = 1, len = arguments.length; j < len; j++) {\r\n\t\tsrc = arguments[j];\r\n\t\tfor (i in src) {\r\n\t\t\tdest[i] = src[i];\r\n\t\t}\r\n\t}\r\n\treturn dest;\r\n}\r\n\r\n// @function create(proto: Object, properties?: Object): Object\r\n// Compatibility polyfill for [Object.create](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/create)\r\nexport var create = Object.create || (function () {\r\n\tfunction F() {}\r\n\treturn function (proto) {\r\n\t\tF.prototype = proto;\r\n\t\treturn new F();\r\n\t};\r\n})();\r\n\r\n// @function bind(fn: Function, …): Function\r\n// Returns a new function bound to the arguments passed, like [Function.prototype.bind](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Function/bind).\r\n// Has a `L.bind()` shortcut.\r\nexport function bind(fn, obj) {\r\n\tvar slice = Array.prototype.slice;\r\n\r\n\tif (fn.bind) {\r\n\t\treturn fn.bind.apply(fn, slice.call(arguments, 1));\r\n\t}\r\n\r\n\tvar args = slice.call(arguments, 2);\r\n\r\n\treturn function () {\r\n\t\treturn fn.apply(obj, args.length ? args.concat(slice.call(arguments)) : arguments);\r\n\t};\r\n}\r\n\r\n// @property lastId: Number\r\n// Last unique ID used by [`stamp()`](#util-stamp)\r\nexport var lastId = 0;\r\n\r\n// @function stamp(obj: Object): Number\r\n// Returns the unique ID of an object, assigning it one if it doesn't have it.\r\nexport function stamp(obj) {\r\n\t/*eslint-disable */\r\n\tobj._leaflet_id = obj._leaflet_id || ++lastId;\r\n\treturn obj._leaflet_id;\r\n\t/* eslint-enable */\r\n}\r\n\r\n// @function throttle(fn: Function, time: Number, context: Object): Function\r\n// Returns a function which executes function `fn` with the given scope `context`\r\n// (so that the `this` keyword refers to `context` inside `fn`'s code). The function\r\n// `fn` will be called no more than one time per given amount of `time`. The arguments\r\n// received by the bound function will be any arguments passed when binding the\r\n// function, followed by any arguments passed when invoking the bound function.\r\n// Has an `L.throttle` shortcut.\r\nexport function throttle(fn, time, context) {\r\n\tvar lock, args, wrapperFn, later;\r\n\r\n\tlater = function () {\r\n\t\t// reset lock and call if queued\r\n\t\tlock = false;\r\n\t\tif (args) {\r\n\t\t\twrapperFn.apply(context, args);\r\n\t\t\targs = false;\r\n\t\t}\r\n\t};\r\n\r\n\twrapperFn = function () {\r\n\t\tif (lock) {\r\n\t\t\t// called too soon, queue to call later\r\n\t\t\targs = arguments;\r\n\r\n\t\t} else {\r\n\t\t\t// call and lock until later\r\n\t\t\tfn.apply(context, arguments);\r\n\t\t\tsetTimeout(later, time);\r\n\t\t\tlock = true;\r\n\t\t}\r\n\t};\r\n\r\n\treturn wrapperFn;\r\n}\r\n\r\n// @function wrapNum(num: Number, range: Number[], includeMax?: Boolean): Number\r\n// Returns the number `num` modulo `range` in such a way so it lies within\r\n// `range[0]` and `range[1]`. The returned value will be always smaller than\r\n// `range[1]` unless `includeMax` is set to `true`.\r\nexport function wrapNum(x, range, includeMax) {\r\n\tvar max = range[1],\r\n\t min = range[0],\r\n\t d = max - min;\r\n\treturn x === max && includeMax ? x : ((x - min) % d + d) % d + min;\r\n}\r\n\r\n// @function falseFn(): Function\r\n// Returns a function which always returns `false`.\r\nexport function falseFn() { return false; }\r\n\r\n// @function formatNum(num: Number, digits?: Number): Number\r\n// Returns the number `num` rounded to `digits` decimals, or to 6 decimals by default.\r\nexport function formatNum(num, digits) {\r\n\tvar pow = Math.pow(10, (digits === undefined ? 6 : digits));\r\n\treturn Math.round(num * pow) / pow;\r\n}\r\n\r\n// @function trim(str: String): String\r\n// Compatibility polyfill for [String.prototype.trim](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim)\r\nexport function trim(str) {\r\n\treturn str.trim ? str.trim() : str.replace(/^\\s+|\\s+$/g, '');\r\n}\r\n\r\n// @function splitWords(str: String): String[]\r\n// Trims and splits the string on whitespace and returns the array of parts.\r\nexport function splitWords(str) {\r\n\treturn trim(str).split(/\\s+/);\r\n}\r\n\r\n// @function setOptions(obj: Object, options: Object): Object\r\n// Merges the given properties to the `options` of the `obj` object, returning the resulting options. See `Class options`. Has an `L.setOptions` shortcut.\r\nexport function setOptions(obj, options) {\r\n\tif (!Object.prototype.hasOwnProperty.call(obj, 'options')) {\r\n\t\tobj.options = obj.options ? create(obj.options) : {};\r\n\t}\r\n\tfor (var i in options) {\r\n\t\tobj.options[i] = options[i];\r\n\t}\r\n\treturn obj.options;\r\n}\r\n\r\n// @function getParamString(obj: Object, existingUrl?: String, uppercase?: Boolean): String\r\n// Converts an object into a parameter URL string, e.g. `{a: \"foo\", b: \"bar\"}`\r\n// translates to `'?a=foo&b=bar'`. If `existingUrl` is set, the parameters will\r\n// be appended at the end. If `uppercase` is `true`, the parameter names will\r\n// be uppercased (e.g. `'?A=foo&B=bar'`)\r\nexport function getParamString(obj, existingUrl, uppercase) {\r\n\tvar params = [];\r\n\tfor (var i in obj) {\r\n\t\tparams.push(encodeURIComponent(uppercase ? i.toUpperCase() : i) + '=' + encodeURIComponent(obj[i]));\r\n\t}\r\n\treturn ((!existingUrl || existingUrl.indexOf('?') === -1) ? '?' : '&') + params.join('&');\r\n}\r\n\r\nvar templateRe = /\\{ *([\\w_-]+) *\\}/g;\r\n\r\n// @function template(str: String, data: Object): String\r\n// Simple templating facility, accepts a template string of the form `'Hello {a}, {b}'`\r\n// and a data object like `{a: 'foo', b: 'bar'}`, returns evaluated string\r\n// `('Hello foo, bar')`. You can also specify functions instead of strings for\r\n// data values — they will be evaluated passing `data` as an argument.\r\nexport function template(str, data) {\r\n\treturn str.replace(templateRe, function (str, key) {\r\n\t\tvar value = data[key];\r\n\r\n\t\tif (value === undefined) {\r\n\t\t\tthrow new Error('No value provided for variable ' + str);\r\n\r\n\t\t} else if (typeof value === 'function') {\r\n\t\t\tvalue = value(data);\r\n\t\t}\r\n\t\treturn value;\r\n\t});\r\n}\r\n\r\n// @function isArray(obj): Boolean\r\n// Compatibility polyfill for [Array.isArray](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray)\r\nexport var isArray = Array.isArray || function (obj) {\r\n\treturn (Object.prototype.toString.call(obj) === '[object Array]');\r\n};\r\n\r\n// @function indexOf(array: Array, el: Object): Number\r\n// Compatibility polyfill for [Array.prototype.indexOf](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf)\r\nexport function indexOf(array, el) {\r\n\tfor (var i = 0; i < array.length; i++) {\r\n\t\tif (array[i] === el) { return i; }\r\n\t}\r\n\treturn -1;\r\n}\r\n\r\n// @property emptyImageUrl: String\r\n// Data URI string containing a base64-encoded empty GIF image.\r\n// Used as a hack to free memory from unused images on WebKit-powered\r\n// mobile devices (by setting image `src` to this string).\r\nexport var emptyImageUrl = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=';\r\n\r\n// inspired by http://paulirish.com/2011/requestanimationframe-for-smart-animating/\r\n\r\nfunction getPrefixed(name) {\r\n\treturn window['webkit' + name] || window['moz' + name] || window['ms' + name];\r\n}\r\n\r\nvar lastTime = 0;\r\n\r\n// fallback for IE 7-8\r\nfunction timeoutDefer(fn) {\r\n\tvar time = +new Date(),\r\n\t timeToCall = Math.max(0, 16 - (time - lastTime));\r\n\r\n\tlastTime = time + timeToCall;\r\n\treturn window.setTimeout(fn, timeToCall);\r\n}\r\n\r\nexport var requestFn = window.requestAnimationFrame || getPrefixed('RequestAnimationFrame') || timeoutDefer;\r\nexport var cancelFn = window.cancelAnimationFrame || getPrefixed('CancelAnimationFrame') ||\r\n\t\tgetPrefixed('CancelRequestAnimationFrame') || function (id) { window.clearTimeout(id); };\r\n\r\n// @function requestAnimFrame(fn: Function, context?: Object, immediate?: Boolean): Number\r\n// Schedules `fn` to be executed when the browser repaints. `fn` is bound to\r\n// `context` if given. When `immediate` is set, `fn` is called immediately if\r\n// the browser doesn't have native support for\r\n// [`window.requestAnimationFrame`](https://developer.mozilla.org/docs/Web/API/window/requestAnimationFrame),\r\n// otherwise it's delayed. Returns a request ID that can be used to cancel the request.\r\nexport function requestAnimFrame(fn, context, immediate) {\r\n\tif (immediate && requestFn === timeoutDefer) {\r\n\t\tfn.call(context);\r\n\t} else {\r\n\t\treturn requestFn.call(window, bind(fn, context));\r\n\t}\r\n}\r\n\r\n// @function cancelAnimFrame(id: Number): undefined\r\n// Cancels a previous `requestAnimFrame`. See also [window.cancelAnimationFrame](https://developer.mozilla.org/docs/Web/API/window/cancelAnimationFrame).\r\nexport function cancelAnimFrame(id) {\r\n\tif (id) {\r\n\t\tcancelFn.call(window, id);\r\n\t}\r\n}\r\n","import * as Util from './Util';\r\n\r\n// @class Class\r\n// @aka L.Class\r\n\r\n// @section\r\n// @uninheritable\r\n\r\n// Thanks to John Resig and Dean Edwards for inspiration!\r\n\r\nexport function Class() {}\r\n\r\nClass.extend = function (props) {\r\n\r\n\t// @function extend(props: Object): Function\r\n\t// [Extends the current class](#class-inheritance) given the properties to be included.\r\n\t// Returns a Javascript function that is a class constructor (to be called with `new`).\r\n\tvar NewClass = function () {\r\n\r\n\t\t// call the constructor\r\n\t\tif (this.initialize) {\r\n\t\t\tthis.initialize.apply(this, arguments);\r\n\t\t}\r\n\r\n\t\t// call all constructor hooks\r\n\t\tthis.callInitHooks();\r\n\t};\r\n\r\n\tvar parentProto = NewClass.__super__ = this.prototype;\r\n\r\n\tvar proto = Util.create(parentProto);\r\n\tproto.constructor = NewClass;\r\n\r\n\tNewClass.prototype = proto;\r\n\r\n\t// inherit parent's statics\r\n\tfor (var i in this) {\r\n\t\tif (Object.prototype.hasOwnProperty.call(this, i) && i !== 'prototype' && i !== '__super__') {\r\n\t\t\tNewClass[i] = this[i];\r\n\t\t}\r\n\t}\r\n\r\n\t// mix static properties into the class\r\n\tif (props.statics) {\r\n\t\tUtil.extend(NewClass, props.statics);\r\n\t\tdelete props.statics;\r\n\t}\r\n\r\n\t// mix includes into the prototype\r\n\tif (props.includes) {\r\n\t\tcheckDeprecatedMixinEvents(props.includes);\r\n\t\tUtil.extend.apply(null, [proto].concat(props.includes));\r\n\t\tdelete props.includes;\r\n\t}\r\n\r\n\t// merge options\r\n\tif (proto.options) {\r\n\t\tprops.options = Util.extend(Util.create(proto.options), props.options);\r\n\t}\r\n\r\n\t// mix given properties into the prototype\r\n\tUtil.extend(proto, props);\r\n\r\n\tproto._initHooks = [];\r\n\r\n\t// add method for calling all hooks\r\n\tproto.callInitHooks = function () {\r\n\r\n\t\tif (this._initHooksCalled) { return; }\r\n\r\n\t\tif (parentProto.callInitHooks) {\r\n\t\t\tparentProto.callInitHooks.call(this);\r\n\t\t}\r\n\r\n\t\tthis._initHooksCalled = true;\r\n\r\n\t\tfor (var i = 0, len = proto._initHooks.length; i < len; i++) {\r\n\t\t\tproto._initHooks[i].call(this);\r\n\t\t}\r\n\t};\r\n\r\n\treturn NewClass;\r\n};\r\n\r\n\r\n// @function include(properties: Object): this\r\n// [Includes a mixin](#class-includes) into the current class.\r\nClass.include = function (props) {\r\n\tUtil.extend(this.prototype, props);\r\n\treturn this;\r\n};\r\n\r\n// @function mergeOptions(options: Object): this\r\n// [Merges `options`](#class-options) into the defaults of the class.\r\nClass.mergeOptions = function (options) {\r\n\tUtil.extend(this.prototype.options, options);\r\n\treturn this;\r\n};\r\n\r\n// @function addInitHook(fn: Function): this\r\n// Adds a [constructor hook](#class-constructor-hooks) to the class.\r\nClass.addInitHook = function (fn) { // (Function) || (String, args...)\r\n\tvar args = Array.prototype.slice.call(arguments, 1);\r\n\r\n\tvar init = typeof fn === 'function' ? fn : function () {\r\n\t\tthis[fn].apply(this, args);\r\n\t};\r\n\r\n\tthis.prototype._initHooks = this.prototype._initHooks || [];\r\n\tthis.prototype._initHooks.push(init);\r\n\treturn this;\r\n};\r\n\r\nfunction checkDeprecatedMixinEvents(includes) {\r\n\tif (typeof L === 'undefined' || !L || !L.Mixin) { return; }\r\n\r\n\tincludes = Util.isArray(includes) ? includes : [includes];\r\n\r\n\tfor (var i = 0; i < includes.length; i++) {\r\n\t\tif (includes[i] === L.Mixin.Events) {\r\n\t\t\tconsole.warn('Deprecated include of L.Mixin.Events: ' +\r\n\t\t\t\t'this property will be removed in future releases, ' +\r\n\t\t\t\t'please inherit from L.Evented instead.', new Error().stack);\r\n\t\t}\r\n\t}\r\n}\r\n","import {Class} from './Class';\r\nimport * as Util from './Util';\r\n\r\n/*\r\n * @class Evented\r\n * @aka L.Evented\r\n * @inherits Class\r\n *\r\n * A set of methods shared between event-powered classes (like `Map` and `Marker`). Generally, events allow you to execute some function when something happens with an object (e.g. the user clicks on the map, causing the map to fire `'click'` event).\r\n *\r\n * @example\r\n *\r\n * ```js\r\n * map.on('click', function(e) {\r\n * \talert(e.latlng);\r\n * } );\r\n * ```\r\n *\r\n * Leaflet deals with event listeners by reference, so if you want to add a listener and then remove it, define it as a function:\r\n *\r\n * ```js\r\n * function onClick(e) { ... }\r\n *\r\n * map.on('click', onClick);\r\n * map.off('click', onClick);\r\n * ```\r\n */\r\n\r\nexport var Events = {\r\n\t/* @method on(type: String, fn: Function, context?: Object): this\r\n\t * Adds a listener function (`fn`) to a particular event type of the object. You can optionally specify the context of the listener (object the this keyword will point to). You can also pass several space-separated types (e.g. `'click dblclick'`).\r\n\t *\r\n\t * @alternative\r\n\t * @method on(eventMap: Object): this\r\n\t * Adds a set of type/listener pairs, e.g. `{click: onClick, mousemove: onMouseMove}`\r\n\t */\r\n\ton: function (types, fn, context) {\r\n\r\n\t\t// types can be a map of types/handlers\r\n\t\tif (typeof types === 'object') {\r\n\t\t\tfor (var type in types) {\r\n\t\t\t\t// we don't process space-separated events here for performance;\r\n\t\t\t\t// it's a hot path since Layer uses the on(obj) syntax\r\n\t\t\t\tthis._on(type, types[type], fn);\r\n\t\t\t}\r\n\r\n\t\t} else {\r\n\t\t\t// types can be a string of space-separated words\r\n\t\t\ttypes = Util.splitWords(types);\r\n\r\n\t\t\tfor (var i = 0, len = types.length; i < len; i++) {\r\n\t\t\t\tthis._on(types[i], fn, context);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn this;\r\n\t},\r\n\r\n\t/* @method off(type: String, fn?: Function, context?: Object): this\r\n\t * Removes a previously added listener function. If no function is specified, it will remove all the listeners of that particular event from the object. Note that if you passed a custom context to `on`, you must pass the same context to `off` in order to remove the listener.\r\n\t *\r\n\t * @alternative\r\n\t * @method off(eventMap: Object): this\r\n\t * Removes a set of type/listener pairs.\r\n\t *\r\n\t * @alternative\r\n\t * @method off: this\r\n\t * Removes all listeners to all events on the object. This includes implicitly attached events.\r\n\t */\r\n\toff: function (types, fn, context) {\r\n\r\n\t\tif (!types) {\r\n\t\t\t// clear all listeners if called without arguments\r\n\t\t\tdelete this._events;\r\n\r\n\t\t} else if (typeof types === 'object') {\r\n\t\t\tfor (var type in types) {\r\n\t\t\t\tthis._off(type, types[type], fn);\r\n\t\t\t}\r\n\r\n\t\t} else {\r\n\t\t\ttypes = Util.splitWords(types);\r\n\r\n\t\t\tfor (var i = 0, len = types.length; i < len; i++) {\r\n\t\t\t\tthis._off(types[i], fn, context);\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// attach listener (without syntactic sugar now)\r\n\t_on: function (type, fn, context) {\r\n\t\tthis._events = this._events || {};\r\n\r\n\t\t/* get/init listeners for type */\r\n\t\tvar typeListeners = this._events[type];\r\n\t\tif (!typeListeners) {\r\n\t\t\ttypeListeners = [];\r\n\t\t\tthis._events[type] = typeListeners;\r\n\t\t}\r\n\r\n\t\tif (context === this) {\r\n\t\t\t// Less memory footprint.\r\n\t\t\tcontext = undefined;\r\n\t\t}\r\n\t\tvar newListener = {fn: fn, ctx: context},\r\n\t\t listeners = typeListeners;\r\n\r\n\t\t// check if fn already there\r\n\t\tfor (var i = 0, len = listeners.length; i < len; i++) {\r\n\t\t\tif (listeners[i].fn === fn && listeners[i].ctx === context) {\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tlisteners.push(newListener);\r\n\t},\r\n\r\n\t_off: function (type, fn, context) {\r\n\t\tvar listeners,\r\n\t\t i,\r\n\t\t len;\r\n\r\n\t\tif (!this._events) { return; }\r\n\r\n\t\tlisteners = this._events[type];\r\n\r\n\t\tif (!listeners) {\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (!fn) {\r\n\t\t\t// Set all removed listeners to noop so they are not called if remove happens in fire\r\n\t\t\tfor (i = 0, len = listeners.length; i < len; i++) {\r\n\t\t\t\tlisteners[i].fn = Util.falseFn;\r\n\t\t\t}\r\n\t\t\t// clear all listeners for a type if function isn't specified\r\n\t\t\tdelete this._events[type];\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tif (context === this) {\r\n\t\t\tcontext = undefined;\r\n\t\t}\r\n\r\n\t\tif (listeners) {\r\n\r\n\t\t\t// find fn and remove it\r\n\t\t\tfor (i = 0, len = listeners.length; i < len; i++) {\r\n\t\t\t\tvar l = listeners[i];\r\n\t\t\t\tif (l.ctx !== context) { continue; }\r\n\t\t\t\tif (l.fn === fn) {\r\n\r\n\t\t\t\t\t// set the removed listener to noop so that's not called if remove happens in fire\r\n\t\t\t\t\tl.fn = Util.falseFn;\r\n\r\n\t\t\t\t\tif (this._firingCount) {\r\n\t\t\t\t\t\t/* copy array in case events are being fired */\r\n\t\t\t\t\t\tthis._events[type] = listeners = listeners.slice();\r\n\t\t\t\t\t}\r\n\t\t\t\t\tlisteners.splice(i, 1);\r\n\r\n\t\t\t\t\treturn;\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t},\r\n\r\n\t// @method fire(type: String, data?: Object, propagate?: Boolean): this\r\n\t// Fires an event of the specified type. You can optionally provide an data\r\n\t// object — the first argument of the listener function will contain its\r\n\t// properties. The event can optionally be propagated to event parents.\r\n\tfire: function (type, data, propagate) {\r\n\t\tif (!this.listens(type, propagate)) { return this; }\r\n\r\n\t\tvar event = Util.extend({}, data, {\r\n\t\t\ttype: type,\r\n\t\t\ttarget: this,\r\n\t\t\tsourceTarget: data && data.sourceTarget || this\r\n\t\t});\r\n\r\n\t\tif (this._events) {\r\n\t\t\tvar listeners = this._events[type];\r\n\r\n\t\t\tif (listeners) {\r\n\t\t\t\tthis._firingCount = (this._firingCount + 1) || 1;\r\n\t\t\t\tfor (var i = 0, len = listeners.length; i < len; i++) {\r\n\t\t\t\t\tvar l = listeners[i];\r\n\t\t\t\t\tl.fn.call(l.ctx || this, event);\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis._firingCount--;\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tif (propagate) {\r\n\t\t\t// propagate the event to parents (set with addEventParent)\r\n\t\t\tthis._propagateEvent(event);\r\n\t\t}\r\n\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method listens(type: String): Boolean\r\n\t// Returns `true` if a particular event type has any listeners attached to it.\r\n\tlistens: function (type, propagate) {\r\n\t\tvar listeners = this._events && this._events[type];\r\n\t\tif (listeners && listeners.length) { return true; }\r\n\r\n\t\tif (propagate) {\r\n\t\t\t// also check parents for listeners if event propagates\r\n\t\t\tfor (var id in this._eventParents) {\r\n\t\t\t\tif (this._eventParents[id].listens(type, propagate)) { return true; }\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn false;\r\n\t},\r\n\r\n\t// @method once(…): this\r\n\t// Behaves as [`on(…)`](#evented-on), except the listener will only get fired once and then removed.\r\n\tonce: function (types, fn, context) {\r\n\r\n\t\tif (typeof types === 'object') {\r\n\t\t\tfor (var type in types) {\r\n\t\t\t\tthis.once(type, types[type], fn);\r\n\t\t\t}\r\n\t\t\treturn this;\r\n\t\t}\r\n\r\n\t\tvar handler = Util.bind(function () {\r\n\t\t\tthis\r\n\t\t\t .off(types, fn, context)\r\n\t\t\t .off(types, handler, context);\r\n\t\t}, this);\r\n\r\n\t\t// add a listener that's executed once and removed after that\r\n\t\treturn this\r\n\t\t .on(types, fn, context)\r\n\t\t .on(types, handler, context);\r\n\t},\r\n\r\n\t// @method addEventParent(obj: Evented): this\r\n\t// Adds an event parent - an `Evented` that will receive propagated events\r\n\taddEventParent: function (obj) {\r\n\t\tthis._eventParents = this._eventParents || {};\r\n\t\tthis._eventParents[Util.stamp(obj)] = obj;\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method removeEventParent(obj: Evented): this\r\n\t// Removes an event parent, so it will stop receiving propagated events\r\n\tremoveEventParent: function (obj) {\r\n\t\tif (this._eventParents) {\r\n\t\t\tdelete this._eventParents[Util.stamp(obj)];\r\n\t\t}\r\n\t\treturn this;\r\n\t},\r\n\r\n\t_propagateEvent: function (e) {\r\n\t\tfor (var id in this._eventParents) {\r\n\t\t\tthis._eventParents[id].fire(e.type, Util.extend({\r\n\t\t\t\tlayer: e.target,\r\n\t\t\t\tpropagatedFrom: e.target\r\n\t\t\t}, e), true);\r\n\t\t}\r\n\t}\r\n};\r\n\r\n// aliases; we should ditch those eventually\r\n\r\n// @method addEventListener(…): this\r\n// Alias to [`on(…)`](#evented-on)\r\nEvents.addEventListener = Events.on;\r\n\r\n// @method removeEventListener(…): this\r\n// Alias to [`off(…)`](#evented-off)\r\n\r\n// @method clearAllEventListeners(…): this\r\n// Alias to [`off()`](#evented-off)\r\nEvents.removeEventListener = Events.clearAllEventListeners = Events.off;\r\n\r\n// @method addOneTimeEventListener(…): this\r\n// Alias to [`once(…)`](#evented-once)\r\nEvents.addOneTimeEventListener = Events.once;\r\n\r\n// @method fireEvent(…): this\r\n// Alias to [`fire(…)`](#evented-fire)\r\nEvents.fireEvent = Events.fire;\r\n\r\n// @method hasEventListeners(…): Boolean\r\n// Alias to [`listens(…)`](#evented-listens)\r\nEvents.hasEventListeners = Events.listens;\r\n\r\nexport var Evented = Class.extend(Events);\r\n","import {isArray, formatNum} from '../core/Util';\r\n\r\n/*\r\n * @class Point\r\n * @aka L.Point\r\n *\r\n * Represents a point with `x` and `y` coordinates in pixels.\r\n *\r\n * @example\r\n *\r\n * ```js\r\n * var point = L.point(200, 300);\r\n * ```\r\n *\r\n * All Leaflet methods and options that accept `Point` objects also accept them in a simple Array form (unless noted otherwise), so these lines are equivalent:\r\n *\r\n * ```js\r\n * map.panBy([200, 300]);\r\n * map.panBy(L.point(200, 300));\r\n * ```\r\n *\r\n * Note that `Point` does not inherit from Leaflet's `Class` object,\r\n * which means new classes can't inherit from it, and new methods\r\n * can't be added to it with the `include` function.\r\n */\r\n\r\nexport function Point(x, y, round) {\r\n\t// @property x: Number; The `x` coordinate of the point\r\n\tthis.x = (round ? Math.round(x) : x);\r\n\t// @property y: Number; The `y` coordinate of the point\r\n\tthis.y = (round ? Math.round(y) : y);\r\n}\r\n\r\nvar trunc = Math.trunc || function (v) {\r\n\treturn v > 0 ? Math.floor(v) : Math.ceil(v);\r\n};\r\n\r\nPoint.prototype = {\r\n\r\n\t// @method clone(): Point\r\n\t// Returns a copy of the current point.\r\n\tclone: function () {\r\n\t\treturn new Point(this.x, this.y);\r\n\t},\r\n\r\n\t// @method add(otherPoint: Point): Point\r\n\t// Returns the result of addition of the current and the given points.\r\n\tadd: function (point) {\r\n\t\t// non-destructive, returns a new point\r\n\t\treturn this.clone()._add(toPoint(point));\r\n\t},\r\n\r\n\t_add: function (point) {\r\n\t\t// destructive, used directly for performance in situations where it's safe to modify existing point\r\n\t\tthis.x += point.x;\r\n\t\tthis.y += point.y;\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method subtract(otherPoint: Point): Point\r\n\t// Returns the result of subtraction of the given point from the current.\r\n\tsubtract: function (point) {\r\n\t\treturn this.clone()._subtract(toPoint(point));\r\n\t},\r\n\r\n\t_subtract: function (point) {\r\n\t\tthis.x -= point.x;\r\n\t\tthis.y -= point.y;\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method divideBy(num: Number): Point\r\n\t// Returns the result of division of the current point by the given number.\r\n\tdivideBy: function (num) {\r\n\t\treturn this.clone()._divideBy(num);\r\n\t},\r\n\r\n\t_divideBy: function (num) {\r\n\t\tthis.x /= num;\r\n\t\tthis.y /= num;\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method multiplyBy(num: Number): Point\r\n\t// Returns the result of multiplication of the current point by the given number.\r\n\tmultiplyBy: function (num) {\r\n\t\treturn this.clone()._multiplyBy(num);\r\n\t},\r\n\r\n\t_multiplyBy: function (num) {\r\n\t\tthis.x *= num;\r\n\t\tthis.y *= num;\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method scaleBy(scale: Point): Point\r\n\t// Multiply each coordinate of the current point by each coordinate of\r\n\t// `scale`. In linear algebra terms, multiply the point by the\r\n\t// [scaling matrix](https://en.wikipedia.org/wiki/Scaling_%28geometry%29#Matrix_representation)\r\n\t// defined by `scale`.\r\n\tscaleBy: function (point) {\r\n\t\treturn new Point(this.x * point.x, this.y * point.y);\r\n\t},\r\n\r\n\t// @method unscaleBy(scale: Point): Point\r\n\t// Inverse of `scaleBy`. Divide each coordinate of the current point by\r\n\t// each coordinate of `scale`.\r\n\tunscaleBy: function (point) {\r\n\t\treturn new Point(this.x / point.x, this.y / point.y);\r\n\t},\r\n\r\n\t// @method round(): Point\r\n\t// Returns a copy of the current point with rounded coordinates.\r\n\tround: function () {\r\n\t\treturn this.clone()._round();\r\n\t},\r\n\r\n\t_round: function () {\r\n\t\tthis.x = Math.round(this.x);\r\n\t\tthis.y = Math.round(this.y);\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method floor(): Point\r\n\t// Returns a copy of the current point with floored coordinates (rounded down).\r\n\tfloor: function () {\r\n\t\treturn this.clone()._floor();\r\n\t},\r\n\r\n\t_floor: function () {\r\n\t\tthis.x = Math.floor(this.x);\r\n\t\tthis.y = Math.floor(this.y);\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method ceil(): Point\r\n\t// Returns a copy of the current point with ceiled coordinates (rounded up).\r\n\tceil: function () {\r\n\t\treturn this.clone()._ceil();\r\n\t},\r\n\r\n\t_ceil: function () {\r\n\t\tthis.x = Math.ceil(this.x);\r\n\t\tthis.y = Math.ceil(this.y);\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method trunc(): Point\r\n\t// Returns a copy of the current point with truncated coordinates (rounded towards zero).\r\n\ttrunc: function () {\r\n\t\treturn this.clone()._trunc();\r\n\t},\r\n\r\n\t_trunc: function () {\r\n\t\tthis.x = trunc(this.x);\r\n\t\tthis.y = trunc(this.y);\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method distanceTo(otherPoint: Point): Number\r\n\t// Returns the cartesian distance between the current and the given points.\r\n\tdistanceTo: function (point) {\r\n\t\tpoint = toPoint(point);\r\n\r\n\t\tvar x = point.x - this.x,\r\n\t\t y = point.y - this.y;\r\n\r\n\t\treturn Math.sqrt(x * x + y * y);\r\n\t},\r\n\r\n\t// @method equals(otherPoint: Point): Boolean\r\n\t// Returns `true` if the given point has the same coordinates.\r\n\tequals: function (point) {\r\n\t\tpoint = toPoint(point);\r\n\r\n\t\treturn point.x === this.x &&\r\n\t\t point.y === this.y;\r\n\t},\r\n\r\n\t// @method contains(otherPoint: Point): Boolean\r\n\t// Returns `true` if both coordinates of the given point are less than the corresponding current point coordinates (in absolute values).\r\n\tcontains: function (point) {\r\n\t\tpoint = toPoint(point);\r\n\r\n\t\treturn Math.abs(point.x) <= Math.abs(this.x) &&\r\n\t\t Math.abs(point.y) <= Math.abs(this.y);\r\n\t},\r\n\r\n\t// @method toString(): String\r\n\t// Returns a string representation of the point for debugging purposes.\r\n\ttoString: function () {\r\n\t\treturn 'Point(' +\r\n\t\t formatNum(this.x) + ', ' +\r\n\t\t formatNum(this.y) + ')';\r\n\t}\r\n};\r\n\r\n// @factory L.point(x: Number, y: Number, round?: Boolean)\r\n// Creates a Point object with the given `x` and `y` coordinates. If optional `round` is set to true, rounds the `x` and `y` values.\r\n\r\n// @alternative\r\n// @factory L.point(coords: Number[])\r\n// Expects an array of the form `[x, y]` instead.\r\n\r\n// @alternative\r\n// @factory L.point(coords: Object)\r\n// Expects a plain object of the form `{x: Number, y: Number}` instead.\r\nexport function toPoint(x, y, round) {\r\n\tif (x instanceof Point) {\r\n\t\treturn x;\r\n\t}\r\n\tif (isArray(x)) {\r\n\t\treturn new Point(x[0], x[1]);\r\n\t}\r\n\tif (x === undefined || x === null) {\r\n\t\treturn x;\r\n\t}\r\n\tif (typeof x === 'object' && 'x' in x && 'y' in x) {\r\n\t\treturn new Point(x.x, x.y);\r\n\t}\r\n\treturn new Point(x, y, round);\r\n}\r\n","import {Point, toPoint} from './Point';\r\n\r\n/*\r\n * @class Bounds\r\n * @aka L.Bounds\r\n *\r\n * Represents a rectangular area in pixel coordinates.\r\n *\r\n * @example\r\n *\r\n * ```js\r\n * var p1 = L.point(10, 10),\r\n * p2 = L.point(40, 60),\r\n * bounds = L.bounds(p1, p2);\r\n * ```\r\n *\r\n * All Leaflet methods that accept `Bounds` objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:\r\n *\r\n * ```js\r\n * otherBounds.intersects([[10, 10], [40, 60]]);\r\n * ```\r\n *\r\n * Note that `Bounds` does not inherit from Leaflet's `Class` object,\r\n * which means new classes can't inherit from it, and new methods\r\n * can't be added to it with the `include` function.\r\n */\r\n\r\nexport function Bounds(a, b) {\r\n\tif (!a) { return; }\r\n\r\n\tvar points = b ? [a, b] : a;\r\n\r\n\tfor (var i = 0, len = points.length; i < len; i++) {\r\n\t\tthis.extend(points[i]);\r\n\t}\r\n}\r\n\r\nBounds.prototype = {\r\n\t// @method extend(point: Point): this\r\n\t// Extends the bounds to contain the given point.\r\n\textend: function (point) { // (Point)\r\n\t\tpoint = toPoint(point);\r\n\r\n\t\t// @property min: Point\r\n\t\t// The top left corner of the rectangle.\r\n\t\t// @property max: Point\r\n\t\t// The bottom right corner of the rectangle.\r\n\t\tif (!this.min && !this.max) {\r\n\t\t\tthis.min = point.clone();\r\n\t\t\tthis.max = point.clone();\r\n\t\t} else {\r\n\t\t\tthis.min.x = Math.min(point.x, this.min.x);\r\n\t\t\tthis.max.x = Math.max(point.x, this.max.x);\r\n\t\t\tthis.min.y = Math.min(point.y, this.min.y);\r\n\t\t\tthis.max.y = Math.max(point.y, this.max.y);\r\n\t\t}\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method getCenter(round?: Boolean): Point\r\n\t// Returns the center point of the bounds.\r\n\tgetCenter: function (round) {\r\n\t\treturn new Point(\r\n\t\t (this.min.x + this.max.x) / 2,\r\n\t\t (this.min.y + this.max.y) / 2, round);\r\n\t},\r\n\r\n\t// @method getBottomLeft(): Point\r\n\t// Returns the bottom-left point of the bounds.\r\n\tgetBottomLeft: function () {\r\n\t\treturn new Point(this.min.x, this.max.y);\r\n\t},\r\n\r\n\t// @method getTopRight(): Point\r\n\t// Returns the top-right point of the bounds.\r\n\tgetTopRight: function () { // -> Point\r\n\t\treturn new Point(this.max.x, this.min.y);\r\n\t},\r\n\r\n\t// @method getTopLeft(): Point\r\n\t// Returns the top-left point of the bounds (i.e. [`this.min`](#bounds-min)).\r\n\tgetTopLeft: function () {\r\n\t\treturn this.min; // left, top\r\n\t},\r\n\r\n\t// @method getBottomRight(): Point\r\n\t// Returns the bottom-right point of the bounds (i.e. [`this.max`](#bounds-max)).\r\n\tgetBottomRight: function () {\r\n\t\treturn this.max; // right, bottom\r\n\t},\r\n\r\n\t// @method getSize(): Point\r\n\t// Returns the size of the given bounds\r\n\tgetSize: function () {\r\n\t\treturn this.max.subtract(this.min);\r\n\t},\r\n\r\n\t// @method contains(otherBounds: Bounds): Boolean\r\n\t// Returns `true` if the rectangle contains the given one.\r\n\t// @alternative\r\n\t// @method contains(point: Point): Boolean\r\n\t// Returns `true` if the rectangle contains the given point.\r\n\tcontains: function (obj) {\r\n\t\tvar min, max;\r\n\r\n\t\tif (typeof obj[0] === 'number' || obj instanceof Point) {\r\n\t\t\tobj = toPoint(obj);\r\n\t\t} else {\r\n\t\t\tobj = toBounds(obj);\r\n\t\t}\r\n\r\n\t\tif (obj instanceof Bounds) {\r\n\t\t\tmin = obj.min;\r\n\t\t\tmax = obj.max;\r\n\t\t} else {\r\n\t\t\tmin = max = obj;\r\n\t\t}\r\n\r\n\t\treturn (min.x >= this.min.x) &&\r\n\t\t (max.x <= this.max.x) &&\r\n\t\t (min.y >= this.min.y) &&\r\n\t\t (max.y <= this.max.y);\r\n\t},\r\n\r\n\t// @method intersects(otherBounds: Bounds): Boolean\r\n\t// Returns `true` if the rectangle intersects the given bounds. Two bounds\r\n\t// intersect if they have at least one point in common.\r\n\tintersects: function (bounds) { // (Bounds) -> Boolean\r\n\t\tbounds = toBounds(bounds);\r\n\r\n\t\tvar min = this.min,\r\n\t\t max = this.max,\r\n\t\t min2 = bounds.min,\r\n\t\t max2 = bounds.max,\r\n\t\t xIntersects = (max2.x >= min.x) && (min2.x <= max.x),\r\n\t\t yIntersects = (max2.y >= min.y) && (min2.y <= max.y);\r\n\r\n\t\treturn xIntersects && yIntersects;\r\n\t},\r\n\r\n\t// @method overlaps(otherBounds: Bounds): Boolean\r\n\t// Returns `true` if the rectangle overlaps the given bounds. Two bounds\r\n\t// overlap if their intersection is an area.\r\n\toverlaps: function (bounds) { // (Bounds) -> Boolean\r\n\t\tbounds = toBounds(bounds);\r\n\r\n\t\tvar min = this.min,\r\n\t\t max = this.max,\r\n\t\t min2 = bounds.min,\r\n\t\t max2 = bounds.max,\r\n\t\t xOverlaps = (max2.x > min.x) && (min2.x < max.x),\r\n\t\t yOverlaps = (max2.y > min.y) && (min2.y < max.y);\r\n\r\n\t\treturn xOverlaps && yOverlaps;\r\n\t},\r\n\r\n\tisValid: function () {\r\n\t\treturn !!(this.min && this.max);\r\n\t}\r\n};\r\n\r\n\r\n// @factory L.bounds(corner1: Point, corner2: Point)\r\n// Creates a Bounds object from two corners coordinate pairs.\r\n// @alternative\r\n// @factory L.bounds(points: Point[])\r\n// Creates a Bounds object from the given array of points.\r\nexport function toBounds(a, b) {\r\n\tif (!a || a instanceof Bounds) {\r\n\t\treturn a;\r\n\t}\r\n\treturn new Bounds(a, b);\r\n}\r\n","import {LatLng, toLatLng} from './LatLng';\r\n\r\n/*\r\n * @class LatLngBounds\r\n * @aka L.LatLngBounds\r\n *\r\n * Represents a rectangular geographical area on a map.\r\n *\r\n * @example\r\n *\r\n * ```js\r\n * var corner1 = L.latLng(40.712, -74.227),\r\n * corner2 = L.latLng(40.774, -74.125),\r\n * bounds = L.latLngBounds(corner1, corner2);\r\n * ```\r\n *\r\n * All Leaflet methods that accept LatLngBounds objects also accept them in a simple Array form (unless noted otherwise), so the bounds example above can be passed like this:\r\n *\r\n * ```js\r\n * map.fitBounds([\r\n * \t[40.712, -74.227],\r\n * \t[40.774, -74.125]\r\n * ]);\r\n * ```\r\n *\r\n * Caution: if the area crosses the antimeridian (often confused with the International Date Line), you must specify corners _outside_ the [-180, 180] degrees longitude range.\r\n *\r\n * Note that `LatLngBounds` does not inherit from Leaflet's `Class` object,\r\n * which means new classes can't inherit from it, and new methods\r\n * can't be added to it with the `include` function.\r\n */\r\n\r\nexport function LatLngBounds(corner1, corner2) { // (LatLng, LatLng) or (LatLng[])\r\n\tif (!corner1) { return; }\r\n\r\n\tvar latlngs = corner2 ? [corner1, corner2] : corner1;\r\n\r\n\tfor (var i = 0, len = latlngs.length; i < len; i++) {\r\n\t\tthis.extend(latlngs[i]);\r\n\t}\r\n}\r\n\r\nLatLngBounds.prototype = {\r\n\r\n\t// @method extend(latlng: LatLng): this\r\n\t// Extend the bounds to contain the given point\r\n\r\n\t// @alternative\r\n\t// @method extend(otherBounds: LatLngBounds): this\r\n\t// Extend the bounds to contain the given bounds\r\n\textend: function (obj) {\r\n\t\tvar sw = this._southWest,\r\n\t\t ne = this._northEast,\r\n\t\t sw2, ne2;\r\n\r\n\t\tif (obj instanceof LatLng) {\r\n\t\t\tsw2 = obj;\r\n\t\t\tne2 = obj;\r\n\r\n\t\t} else if (obj instanceof LatLngBounds) {\r\n\t\t\tsw2 = obj._southWest;\r\n\t\t\tne2 = obj._northEast;\r\n\r\n\t\t\tif (!sw2 || !ne2) { return this; }\r\n\r\n\t\t} else {\r\n\t\t\treturn obj ? this.extend(toLatLng(obj) || toLatLngBounds(obj)) : this;\r\n\t\t}\r\n\r\n\t\tif (!sw && !ne) {\r\n\t\t\tthis._southWest = new LatLng(sw2.lat, sw2.lng);\r\n\t\t\tthis._northEast = new LatLng(ne2.lat, ne2.lng);\r\n\t\t} else {\r\n\t\t\tsw.lat = Math.min(sw2.lat, sw.lat);\r\n\t\t\tsw.lng = Math.min(sw2.lng, sw.lng);\r\n\t\t\tne.lat = Math.max(ne2.lat, ne.lat);\r\n\t\t\tne.lng = Math.max(ne2.lng, ne.lng);\r\n\t\t}\r\n\r\n\t\treturn this;\r\n\t},\r\n\r\n\t// @method pad(bufferRatio: Number): LatLngBounds\r\n\t// Returns bounds created by extending or retracting the current bounds by a given ratio in each direction.\r\n\t// For example, a ratio of 0.5 extends the bounds by 50% in each direction.\r\n\t// Negative values will retract the bounds.\r\n\tpad: function (bufferRatio) {\r\n\t\tvar sw = this._southWest,\r\n\t\t ne = this._northEast,\r\n\t\t heightBuffer = Math.abs(sw.lat - ne.lat) * bufferRatio,\r\n\t\t widthBuffer = Math.abs(sw.lng - ne.lng) * bufferRatio;\r\n\r\n\t\treturn new LatLngBounds(\r\n\t\t new LatLng(sw.lat - heightBuffer, sw.lng - widthBuffer),\r\n\t\t new LatLng(ne.lat + heightBuffer, ne.lng + widthBuffer));\r\n\t},\r\n\r\n\t// @method getCenter(): LatLng\r\n\t// Returns the center point of the bounds.\r\n\tgetCenter: function () {\r\n\t\treturn new LatLng(\r\n\t\t (this._southWest.lat + this._northEast.lat) / 2,\r\n\t\t (this._southWest.lng + this._northEast.lng) / 2);\r\n\t},\r\n\r\n\t// @method getSouthWest(): LatLng\r\n\t// Returns the south-west point of the bounds.\r\n\tgetSouthWest: function () {\r\n\t\treturn this._southWest;\r\n\t},\r\n\r\n\t// @method getNorthEast(): LatLng\r\n\t// Returns the north-east point of the bounds.\r\n\tgetNorthEast: function () {\r\n\t\treturn this._northEast;\r\n\t},\r\n\r\n\t// @method getNorthWest(): LatLng\r\n\t// Returns the north-west point of the bounds.\r\n\tgetNorthWest: function () {\r\n\t\treturn new LatLng(this.getNorth(), this.getWest());\r\n\t},\r\n\r\n\t// @method getSouthEast(): LatLng\r\n\t// Returns the south-east point of the bounds.\r\n\tgetSouthEast: function () {\r\n\t\treturn new LatLng(this.getSouth(), this.getEast());\r\n\t},\r\n\r\n\t// @method getWest(): Number\r\n\t// Returns the west longitude of the bounds\r\n\tgetWest: function () {\r\n\t\treturn this._southWest.lng;\r\n\t},\r\n\r\n\t// @method getSouth(): Number\r\n\t// Returns the south latitude of the bounds\r\n\tgetSouth: function () {\r\n\t\treturn this._southWest.lat;\r\n\t},\r\n\r\n\t// @method getEast(): Number\r\n\t// Returns the east longitude of the bounds\r\n\tgetEast: function () {\r\n\t\treturn this._northEast.lng;\r\n\t},\r\n\r\n\t// @method getNorth(): Number\r\n\t// Returns the north latitude of the bounds\r\n\tgetNorth: function () {\r\n\t\treturn this._northEast.lat;\r\n\t},\r\n\r\n\t// @method contains(otherBounds: LatLngBounds): Boolean\r\n\t// Returns `true` if the rectangle contains the given one.\r\n\r\n\t// @alternative\r\n\t// @method contains (latlng: LatLng): Boolean\r\n\t// Returns `true` if the rectangle contains the given point.\r\n\tcontains: function (obj) { // (LatLngBounds) or (LatLng) -> Boolean\r\n\t\tif (typeof obj[0] === 'number' || obj instanceof LatLng || 'lat' in obj) {\r\n\t\t\tobj = toLatLng(obj);\r\n\t\t} else {\r\n\t\t\tobj = toLatLngBounds(obj);\r\n\t\t}\r\n\r\n\t\tvar sw = this._southWest,\r\n\t\t ne = this._northEast,\r\n\t\t sw2, ne2;\r\n\r\n\t\tif (obj instanceof LatLngBounds) {\r\n\t\t\tsw2 = obj.getSouthWest();\r\n\t\t\tne2 = obj.getNorthEast();\r\n\t\t} else {\r\n\t\t\tsw2 = ne2 = obj;\r\n\t\t}\r\n\r\n\t\treturn (sw2.lat >= sw.lat) && (ne2.lat <= ne.lat) &&\r\n\t\t (sw2.lng >= sw.lng) && (ne2.lng <= ne.lng);\r\n\t},\r\n\r\n\t// @method intersects(otherBounds: LatLngBounds): Boolean\r\n\t// Returns `true` if the rectangle intersects the given bounds. Two bounds intersect if they have at least one point in common.\r\n\tintersects: function (bounds) {\r\n\t\tbounds = toLatLngBounds(bounds);\r\n\r\n\t\tvar sw = this._southWest,\r\n\t\t ne = this._northEast,\r\n\t\t sw2 = bounds.getSouthWest(),\r\n\t\t ne2 = bounds.getNorthEast(),\r\n\r\n\t\t latIntersects = (ne2.lat >= sw.lat) && (sw2.lat <= ne.lat),\r\n\t\t lngIntersects = (ne2.lng >= sw.lng) && (sw2.lng <= ne.lng);\r\n\r\n\t\treturn latIntersects && lngIntersects;\r\n\t},\r\n\r\n\t// @method overlaps(otherBounds: LatLngBounds): Boolean\r\n\t// Returns `true` if the rectangle overlaps the given bounds. Two bounds overlap if their intersection is an area.\r\n\toverlaps: function (bounds) {\r\n\t\tbounds = toLatLngBounds(bounds);\r\n\r\n\t\tvar sw = this._southWest,\r\n\t\t ne = this._northEast,\r\n\t\t sw2 = bounds.getSouthWest(),\r\n\t\t ne2 = bounds.getNorthEast(),\r\n\r\n\t\t latOverlaps = (ne2.lat > sw.lat) && (sw2.lat < ne.lat),\r\n\t\t lngOverlaps = (ne2.lng > sw.lng) && (sw2.lng < ne.lng);\r\n\r\n\t\treturn latOverlaps && lngOverlaps;\r\n\t},\r\n\r\n\t// @method toBBoxString(): String\r\n\t// Returns a string with bounding box coordinates in a 'southwest_lng,southwest_lat,northeast_lng,northeast_lat' format. Useful for sending requests to web services that return geo data.\r\n\ttoBBoxString: function () {\r\n\t\treturn [this.getWest(), this.getSouth(), this.getEast(), this.getNorth()].join(',');\r\n\t},\r\n\r\n\t// @method equals(otherBounds: LatLngBounds, maxMargin?: Number): Boolean\r\n\t// Returns `true` if the rectangle is equivalent (within a small margin of error) to the given bounds. The margin of error can be overridden by setting `maxMargin` to a small number.\r\n\tequals: function (bounds, maxMargin) {\r\n\t\tif (!bounds) { return false; }\r\n\r\n\t\tbounds = toLatLngBounds(bounds);\r\n\r\n\t\treturn this._southWest.equals(bounds.getSouthWest(), maxMargin) &&\r\n\t\t this._northEast.equals(bounds.getNorthEast(), maxMargin);\r\n\t},\r\n\r\n\t// @method isValid(): Boolean\r\n\t// Returns `true` if the bounds are properly initialized.\r\n\tisValid: function () {\r\n\t\treturn !!(this._southWest && this._northEast);\r\n\t}\r\n};\r\n\r\n// TODO International date line?\r\n\r\n// @factory L.latLngBounds(corner1: LatLng, corner2: LatLng)\r\n// Creates a `LatLngBounds` object by defining two diagonally opposite corners of the rectangle.\r\n\r\n// @alternative\r\n// @factory L.latLngBounds(latlngs: LatLng[])\r\n// Creates a `LatLngBounds` object defined by the geographical points it contains. Very useful for zooming the map to fit a particular set of locations with [`fitBounds`](#map-fitbounds).\r\nexport function toLatLngBounds(a, b) {\r\n\tif (a instanceof LatLngBounds) {\r\n\t\treturn a;\r\n\t}\r\n\treturn new LatLngBounds(a, b);\r\n}\r\n","import * as Util from '../core/Util';\r\nimport {Earth} from './crs/CRS.Earth';\r\nimport {toLatLngBounds} from './LatLngBounds';\r\n\r\n/* @class LatLng\r\n * @aka L.LatLng\r\n *\r\n * Represents a geographical point with a certain latitude and longitude.\r\n *\r\n * @example\r\n *\r\n * ```\r\n * var latlng = L.latLng(50.5, 30.5);\r\n * ```\r\n *\r\n * All Leaflet methods that accept LatLng objects also accept them in a simple Array form and simple object form (unless noted otherwise), so these lines are equivalent:\r\n *\r\n * ```\r\n * map.panTo([50, 30]);\r\n * map.panTo({lon: 30, lat: 50});\r\n * map.panTo({lat: 50, lng: 30});\r\n * map.panTo(L.latLng(50, 30));\r\n * ```\r\n *\r\n * Note that `LatLng` does not inherit from Leaflet's `Class` object,\r\n * which means new classes can't inherit from it, and new methods\r\n * can't be added to it with the `include` function.\r\n */\r\n\r\nexport function LatLng(lat, lng, alt) {\r\n\tif (isNaN(lat) || isNaN(lng)) {\r\n\t\tthrow new Error('Invalid LatLng object: (' + lat + ', ' + lng + ')');\r\n\t}\r\n\r\n\t// @property lat: Number\r\n\t// Latitude in degrees\r\n\tthis.lat = +lat;\r\n\r\n\t// @property lng: Number\r\n\t// Longitude in degrees\r\n\tthis.lng = +lng;\r\n\r\n\t// @property alt: Number\r\n\t// Altitude in meters (optional)\r\n\tif (alt !== undefined) {\r\n\t\tthis.alt = +alt;\r\n\t}\r\n}\r\n\r\nLatLng.prototype = {\r\n\t// @method equals(otherLatLng: LatLng, maxMargin?: Number): Boolean\r\n\t// Returns `true` if the given `LatLng` point is at the same position (within a small margin of error). The margin of error can be overridden by setting `maxMargin` to a small number.\r\n\tequals: function (obj, maxMargin) {\r\n\t\tif (!obj) { return false; }\r\n\r\n\t\tobj = toLatLng(obj);\r\n\r\n\t\tvar margin = Math.max(\r\n\t\t Math.abs(this.lat - obj.lat),\r\n\t\t Math.abs(this.lng - obj.lng));\r\n\r\n\t\treturn margin <= (maxMargin === undefined ? 1.0E-9 : maxMargin);\r\n\t},\r\n\r\n\t// @method toString(): String\r\n\t// Returns a string representation of the point (for debugging purposes).\r\n\ttoString: function (precision) {\r\n\t\treturn 'LatLng(' +\r\n\t\t Util.formatNum(this.lat, precision) + ', ' +\r\n\t\t Util.formatNum(this.lng, precision) + ')';\r\n\t},\r\n\r\n\t// @method distanceTo(otherLatLng: LatLng): Number\r\n\t// Returns the distance (in meters) to the given `LatLng` calculated using the [Spherical Law of Cosines](https://en.wikipedia.org/wiki/Spherical_law_of_cosines).\r\n\tdistanceTo: function (other) {\r\n\t\treturn Earth.distance(this, toLatLng(other));\r\n\t},\r\n\r\n\t// @method wrap(): LatLng\r\n\t// Returns a new `LatLng` object with the longitude wrapped so it's always between -180 and +180 degrees.\r\n\twrap: function () {\r\n\t\treturn Earth.wrapLatLng(this);\r\n\t},\r\n\r\n\t// @method toBounds(sizeInMeters: Number): LatLngBounds\r\n\t// Returns a new `LatLngBounds` object in which each boundary is `sizeInMeters/2` meters apart from the `LatLng`.\r\n\ttoBounds: function (sizeInMeters) {\r\n\t\tvar latAccuracy = 180 * sizeInMeters / 40075017,\r\n\t\t lngAccuracy = latAccuracy / Math.cos((Math.PI / 180) * this.lat);\r\n\r\n\t\treturn toLatLngBounds(\r\n\t\t [this.lat - latAccuracy, this.lng - lngAccuracy],\r\n\t\t [this.lat + latAccuracy, this.lng + lngAccuracy]);\r\n\t},\r\n\r\n\tclone: function () {\r\n\t\treturn new LatLng(this.lat, this.lng, this.alt);\r\n\t}\r\n};\r\n\r\n\r\n\r\n// @factory L.latLng(latitude: Number, longitude: Number, altitude?: Number): LatLng\r\n// Creates an object representing a geographical point with the given latitude and longitude (and optionally altitude).\r\n\r\n// @alternative\r\n// @factory L.latLng(coords: Array): LatLng\r\n// Expects an array of the form `[Number, Number]` or `[Number, Number, Number]` instead.\r\n\r\n// @alternative\r\n// @factory L.latLng(coords: Object): LatLng\r\n// Expects an plain object of the form `{lat: Number, lng: Number}` or `{lat: Number, lng: Number, alt: Number}` instead.\r\n\r\nexport function toLatLng(a, b, c) {\r\n\tif (a instanceof LatLng) {\r\n\t\treturn a;\r\n\t}\r\n\tif (Util.isArray(a) && typeof a[0] !== 'object') {\r\n\t\tif (a.length === 3) {\r\n\t\t\treturn new LatLng(a[0], a[1], a[2]);\r\n\t\t}\r\n\t\tif (a.length === 2) {\r\n\t\t\treturn new LatLng(a[0], a[1]);\r\n\t\t}\r\n\t\treturn null;\r\n\t}\r\n\tif (a === undefined || a === null) {\r\n\t\treturn a;\r\n\t}\r\n\tif (typeof a === 'object' && 'lat' in a) {\r\n\t\treturn new LatLng(a.lat, 'lng' in a ? a.lng : a.lon, a.alt);\r\n\t}\r\n\tif (b === undefined) {\r\n\t\treturn null;\r\n\t}\r\n\treturn new LatLng(a, b, c);\r\n}\r\n","\r\nimport {Bounds} from '../../geometry/Bounds';\r\nimport {LatLng} from '../LatLng';\r\nimport {LatLngBounds} from '../LatLngBounds';\r\nimport * as Util from '../../core/Util';\r\n\r\n/*\r\n * @namespace CRS\r\n * @crs L.CRS.Base\r\n * Object that defines coordinate reference systems for projecting\r\n * geographical points into pixel (screen) coordinates and back (and to\r\n * coordinates in other units for [WMS](https://en.wikipedia.org/wiki/Web_Map_Service) services). See\r\n * [spatial reference system](http://en.wikipedia.org/wiki/Coordinate_reference_system).\r\n *\r\n * Leaflet defines the most usual CRSs by default. If you want to use a\r\n * CRS not defined by default, take a look at the\r\n * [Proj4Leaflet](https://github.com/kartena/Proj4Leaflet) plugin.\r\n *\r\n * Note that the CRS instances do not inherit from Leaflet's `Class` object,\r\n * and can't be instantiated. Also, new classes can't inherit from them,\r\n * and methods can't be added to them with the `include` function.\r\n */\r\n\r\nexport var CRS = {\r\n\t// @method latLngToPoint(latlng: LatLng, zoom: Number): Point\r\n\t// Projects geographical coordinates into pixel coordinates for a given zoom.\r\n\tlatLngToPoint: function (latlng, zoom) {\r\n\t\tvar projectedPoint = this.projection.project(latlng),\r\n\t\t scale = this.scale(zoom);\r\n\r\n\t\treturn this.transformation._transform(projectedPoint, scale);\r\n\t},\r\n\r\n\t// @method pointToLatLng(point: Point, zoom: Number): LatLng\r\n\t// The inverse of `latLngToPoint`. Projects pixel coordinates on a given\r\n\t// zoom into geographical coordinates.\r\n\tpointToLatLng: function (point, zoom) {\r\n\t\tvar scale = this.scale(zoom),\r\n\t\t untransformedPoint = this.transformation.untransform(point, scale);\r\n\r\n\t\treturn this.projection.unproject(untransformedPoint);\r\n\t},\r\n\r\n\t// @method project(latlng: LatLng): Point\r\n\t// Projects geographical coordinates into coordinates in units accepted for\r\n\t// this CRS (e.g. meters for EPSG:3857, for passing it to WMS services).\r\n\tproject: function (latlng) {\r\n\t\treturn this.projection.project(latlng);\r\n\t},\r\n\r\n\t// @method unproject(point: Point): LatLng\r\n\t// Given a projected coordinate returns the corresponding LatLng.\r\n\t// The inverse of `project`.\r\n\tunproject: function (point) {\r\n\t\treturn this.projection.unproject(point);\r\n\t},\r\n\r\n\t// @method scale(zoom: Number): Number\r\n\t// Returns the scale used when transforming projected coordinates into\r\n\t// pixel coordinates for a particular zoom. For example, it returns\r\n\t// `256 * 2^zoom` for Mercator-based CRS.\r\n\tscale: function (zoom) {\r\n\t\treturn 256 * Math.pow(2, zoom);\r\n\t},\r\n\r\n\t// @method zoom(scale: Number): Number\r\n\t// Inverse of `scale()`, returns the zoom level corresponding to a scale\r\n\t// factor of `scale`.\r\n\tzoom: function (scale) {\r\n\t\treturn Math.log(scale / 256) / Math.LN2;\r\n\t},\r\n\r\n\t// @method getProjectedBounds(zoom: Number): Bounds\r\n\t// Returns the projection's bounds scaled and transformed for the provided `zoom`.\r\n\tgetProjectedBounds: function (zoom) {\r\n\t\tif (this.infinite) { return null; }\r\n\r\n\t\tvar b = this.projection.bounds,\r\n\t\t s = this.scale(zoom),\r\n\t\t min = this.transformation.transform(b.min, s),\r\n\t\t max = this.transformation.transform(b.max, s);\r\n\r\n\t\treturn new Bounds(min, max);\r\n\t},\r\n\r\n\t// @method distance(latlng1: LatLng, latlng2: LatLng): Number\r\n\t// Returns the distance between two geographical coordinates.\r\n\r\n\t// @property code: String\r\n\t// Standard code name of the CRS passed into WMS services (e.g. `'EPSG:3857'`)\r\n\t//\r\n\t// @property wrapLng: Number[]\r\n\t// An array of two numbers defining whether the longitude (horizontal) coordinate\r\n\t// axis wraps around a given range and how. Defaults to `[-180, 180]` in most\r\n\t// geographical CRSs. If `undefined`, the longitude axis does not wrap around.\r\n\t//\r\n\t// @property wrapLat: Number[]\r\n\t// Like `wrapLng`, but for the latitude (vertical) axis.\r\n\r\n\t// wrapLng: [min, max],\r\n\t// wrapLat: [min, max],\r\n\r\n\t// @property infinite: Boolean\r\n\t// If true, the coordinate space will be unbounded (infinite in both axes)\r\n\tinfinite: false,\r\n\r\n\t// @method wrapLatLng(latlng: LatLng): LatLng\r\n\t// Returns a `LatLng` where lat and lng has been wrapped according to the\r\n\t// CRS's `wrapLat` and `wrapLng` properties, if they are outside the CRS's bounds.\r\n\twrapLatLng: function (latlng) {\r\n\t\tvar lng = this.wrapLng ? Util.wrapNum(latlng.lng, this.wrapLng, true) : latlng.lng,\r\n\t\t lat = this.wrapLat ? Util.wrapNum(latlng.lat, this.wrapLat, true) : latlng.lat,\r\n\t\t alt = latlng.alt;\r\n\r\n\t\treturn new LatLng(lat, lng, alt);\r\n\t},\r\n\r\n\t// @method wrapLatLngBounds(bounds: LatLngBounds): LatLngBounds\r\n\t// Returns a `LatLngBounds` with the same size as the given one, ensuring\r\n\t// that its center is within the CRS's bounds.\r\n\t// Only accepts actual `L.LatLngBounds` instances, not arrays.\r\n\twrapLatLngBounds: function (bounds) {\r\n\t\tvar center = bounds.getCenter(),\r\n\t\t newCenter = this.wrapLatLng(center),\r\n\t\t latShift = center.lat - newCenter.lat,\r\n\t\t lngShift = center.lng - newCenter.lng;\r\n\r\n\t\tif (latShift === 0 && lngShift === 0) {\r\n\t\t\treturn bounds;\r\n\t\t}\r\n\r\n\t\tvar sw = bounds.getSouthWest(),\r\n\t\t ne = bounds.getNorthEast(),\r\n\t\t newSw = new LatLng(sw.lat - latShift, sw.lng - lngShift),\r\n\t\t newNe = new LatLng(ne.lat - latShift, ne.lng - lngShift);\r\n\r\n\t\treturn new LatLngBounds(newSw, newNe);\r\n\t}\r\n};\r\n","import {CRS} from './CRS';\nimport * as Util from '../../core/Util';\n\n/*\n * @namespace CRS\n * @crs L.CRS.Earth\n *\n * Serves as the base for CRS that are global such that they cover the earth.\n * Can only be used as the base for other CRS and cannot be used directly,\n * since it does not have a `code`, `projection` or `transformation`. `distance()` returns\n * meters.\n */\n\nexport var Earth = Util.extend({}, CRS, {\n\twrapLng: [-180, 180],\n\n\t// Mean Earth Radius, as recommended for use by\n\t// the International Union of Geodesy and Geophysics,\n\t// see http://rosettacode.org/wiki/Haversine_formula\n\tR: 6371000,\n\n\t// distance between two geographical points using spherical law of cosines approximation\n\tdistance: function (latlng1, latlng2) {\n\t\tvar rad = Math.PI / 180,\n\t\t lat1 = latlng1.lat * rad,\n\t\t lat2 = latlng2.lat * rad,\n\t\t sinDLat = Math.sin((latlng2.lat - latlng1.lat) * rad / 2),\n\t\t sinDLon = Math.sin((latlng2.lng - latlng1.lng) * rad / 2),\n\t\t a = sinDLat * sinDLat + Math.cos(lat1) * Math.cos(lat2) * sinDLon * sinDLon,\n\t\t c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\t\treturn this.R * c;\n\t}\n});\n","import {LatLng} from '../LatLng';\r\nimport {Bounds} from '../../geometry/Bounds';\r\nimport {Point} from '../../geometry/Point';\r\n\r\n/*\r\n * @namespace Projection\r\n * @projection L.Projection.SphericalMercator\r\n *\r\n * Spherical Mercator projection — the most common projection for online maps,\r\n * used by almost all free and commercial tile providers. Assumes that Earth is\r\n * a sphere. Used by the `EPSG:3857` CRS.\r\n */\r\n\r\nvar earthRadius = 6378137;\r\n\r\nexport var SphericalMercator = {\r\n\r\n\tR: earthRadius,\r\n\tMAX_LATITUDE: 85.0511287798,\r\n\r\n\tproject: function (latlng) {\r\n\t\tvar d = Math.PI / 180,\r\n\t\t max = this.MAX_LATITUDE,\r\n\t\t lat = Math.max(Math.min(max, latlng.lat), -max),\r\n\t\t sin = Math.sin(lat * d);\r\n\r\n\t\treturn new Point(\r\n\t\t\tthis.R * latlng.lng * d,\r\n\t\t\tthis.R * Math.log((1 + sin) / (1 - sin)) / 2);\r\n\t},\r\n\r\n\tunproject: function (point) {\r\n\t\tvar d = 180 / Math.PI;\r\n\r\n\t\treturn new LatLng(\r\n\t\t\t(2 * Math.atan(Math.exp(point.y / this.R)) - (Math.PI / 2)) * d,\r\n\t\t\tpoint.x * d / this.R);\r\n\t},\r\n\r\n\tbounds: (function () {\r\n\t\tvar d = earthRadius * Math.PI;\r\n\t\treturn new Bounds([-d, -d], [d, d]);\r\n\t})()\r\n};\r\n","import {Point} from './Point';\r\nimport * as Util from '../core/Util';\r\n\r\n/*\r\n * @class Transformation\r\n * @aka L.Transformation\r\n *\r\n * Represents an affine transformation: a set of coefficients `a`, `b`, `c`, `d`\r\n * for transforming a point of a form `(x, y)` into `(a*x + b, c*y + d)` and doing\r\n * the reverse. Used by Leaflet in its projections code.\r\n *\r\n * @example\r\n *\r\n * ```js\r\n * var transformation = L.transformation(2, 5, -1, 10),\r\n * \tp = L.point(1, 2),\r\n * \tp2 = transformation.transform(p), // L.point(7, 8)\r\n * \tp3 = transformation.untransform(p2); // L.point(1, 2)\r\n * ```\r\n */\r\n\r\n\r\n// factory new L.Transformation(a: Number, b: Number, c: Number, d: Number)\r\n// Creates a `Transformation` object with the given coefficients.\r\nexport function Transformation(a, b, c, d) {\r\n\tif (Util.isArray(a)) {\r\n\t\t// use array properties\r\n\t\tthis._a = a[0];\r\n\t\tthis._b = a[1];\r\n\t\tthis._c = a[2];\r\n\t\tthis._d = a[3];\r\n\t\treturn;\r\n\t}\r\n\tthis._a = a;\r\n\tthis._b = b;\r\n\tthis._c = c;\r\n\tthis._d = d;\r\n}\r\n\r\nTransformation.prototype = {\r\n\t// @method transform(point: Point, scale?: Number): Point\r\n\t// Returns a transformed point, optionally multiplied by the given scale.\r\n\t// Only accepts actual `L.Point` instances, not arrays.\r\n\ttransform: function (point, scale) { // (Point, Number) -> Point\r\n\t\treturn this._transform(point.clone(), scale);\r\n\t},\r\n\r\n\t// destructive transform (faster)\r\n\t_transform: function (point, scale) {\r\n\t\tscale = scale || 1;\r\n\t\tpoint.x = scale * (this._a * point.x + this._b);\r\n\t\tpoint.y = scale * (this._c * point.y + this._d);\r\n\t\treturn point;\r\n\t},\r\n\r\n\t// @method untransform(point: Point, scale?: Number): Point\r\n\t// Returns the reverse transformation of the given point, optionally divided\r\n\t// by the given scale. Only accepts actual `L.Point` instances, not arrays.\r\n\tuntransform: function (point, scale) {\r\n\t\tscale = scale || 1;\r\n\t\treturn new Point(\r\n\t\t (point.x / scale - this._b) / this._a,\r\n\t\t (point.y / scale - this._d) / this._c);\r\n\t}\r\n};\r\n\r\n// factory L.transformation(a: Number, b: Number, c: Number, d: Number)\r\n\r\n// @factory L.transformation(a: Number, b: Number, c: Number, d: Number)\r\n// Instantiates a Transformation object with the given coefficients.\r\n\r\n// @alternative\r\n// @factory L.transformation(coefficients: Array): Transformation\r\n// Expects an coefficients array of the form\r\n// `[a: Number, b: Number, c: Number, d: Number]`.\r\n\r\nexport function toTransformation(a, b, c, d) {\r\n\treturn new Transformation(a, b, c, d);\r\n}\r\n","import {Earth} from './CRS.Earth';\r\nimport {SphericalMercator} from '../projection/Projection.SphericalMercator';\r\nimport {toTransformation} from '../../geometry/Transformation';\r\nimport * as Util from '../../core/Util';\r\n\r\n/*\r\n * @namespace CRS\r\n * @crs L.CRS.EPSG3857\r\n *\r\n * The most common CRS for online maps, used by almost all free and commercial\r\n * tile providers. Uses Spherical Mercator projection. Set in by default in\r\n * Map's `crs` option.\r\n */\r\n\r\nexport var EPSG3857 = Util.extend({}, Earth, {\r\n\tcode: 'EPSG:3857',\r\n\tprojection: SphericalMercator,\r\n\r\n\ttransformation: (function () {\r\n\t\tvar scale = 0.5 / (Math.PI * SphericalMercator.R);\r\n\t\treturn toTransformation(scale, 0.5, -scale, 0.5);\r\n\t}())\r\n});\r\n\r\nexport var EPSG900913 = Util.extend({}, EPSG3857, {\r\n\tcode: 'EPSG:900913'\r\n});\r\n","import * as Browser from '../../core/Browser';\n\n// @namespace SVG; @section\n// There are several static functions which can be called without instantiating L.SVG:\n\n// @function create(name: String): SVGElement\n// Returns a instance of [SVGElement](https://developer.mozilla.org/docs/Web/API/SVGElement),\n// corresponding to the class name passed. For example, using 'line' will return\n// an instance of [SVGLineElement](https://developer.mozilla.org/docs/Web/API/SVGLineElement).\nexport function svgCreate(name) {\n\treturn document.createElementNS('http://www.w3.org/2000/svg', name);\n}\n\n// @function pointsToPath(rings: Point[], closed: Boolean): String\n// Generates a SVG path string for multiple rings, with each ring turning\n// into \"M..L..L..\" instructions\nexport function pointsToPath(rings, closed) {\n\tvar str = '',\n\ti, j, len, len2, points, p;\n\n\tfor (i = 0, len = rings.length; i < len; i++) {\n\t\tpoints = rings[i];\n\n\t\tfor (j = 0, len2 = points.length; j < len2; j++) {\n\t\t\tp = points[j];\n\t\t\tstr += (j ? 'L' : 'M') + p.x + ' ' + p.y;\n\t\t}\n\n\t\t// closes the ring for polygons; \"x\" is VML syntax\n\t\tstr += closed ? (Browser.svg ? 'z' : 'x') : '';\n\t}\n\n\t// SVG complains about empty path strings\n\treturn str || 'M0 0';\n}\n\n\n\n\n","import * as Util from './Util';\r\nimport {svgCreate} from '../layer/vector/SVG.Util';\r\n\r\n/*\r\n * @namespace Browser\r\n * @aka L.Browser\r\n *\r\n * A namespace with static properties for browser/feature detection used by Leaflet internally.\r\n *\r\n * @example\r\n *\r\n * ```js\r\n * if (L.Browser.ielt9) {\r\n * alert('Upgrade your browser, dude!');\r\n * }\r\n * ```\r\n */\r\n\r\nvar style = document.documentElement.style;\r\n\r\n// @property ie: Boolean; `true` for all Internet Explorer versions (not Edge).\r\nexport var ie = 'ActiveXObject' in window;\r\n\r\n// @property ielt9: Boolean; `true` for Internet Explorer versions less than 9.\r\nexport var ielt9 = ie && !document.addEventListener;\r\n\r\n// @property edge: Boolean; `true` for the Edge web browser.\r\nexport var edge = 'msLaunchUri' in navigator && !('documentMode' in document);\r\n\r\n// @property webkit: Boolean;\r\n// `true` for webkit-based browsers like Chrome and Safari (including mobile versions).\r\nexport var webkit = userAgentContains('webkit');\r\n\r\n// @property android: Boolean\r\n// `true` for any browser running on an Android platform.\r\nexport var android = userAgentContains('android');\r\n\r\n// @property android23: Boolean; `true` for browsers running on Android 2 or Android 3.\r\nexport var android23 = userAgentContains('android 2') || userAgentContains('android 3');\r\n\r\n/* See https://stackoverflow.com/a/17961266 for details on detecting stock Android */\r\nvar webkitVer = parseInt(/WebKit\\/([0-9]+)|$/.exec(navigator.userAgent)[1], 10); // also matches AppleWebKit\r\n// @property androidStock: Boolean; `true` for the Android stock browser (i.e. not Chrome)\r\nexport var androidStock = android && userAgentContains('Google') && webkitVer < 537 && !('AudioNode' in window);\r\n\r\n// @property opera: Boolean; `true` for the Opera browser\r\nexport var opera = !!window.opera;\r\n\r\n// @property chrome: Boolean; `true` for the Chrome browser.\r\nexport var chrome = !edge && userAgentContains('chrome');\r\n\r\n// @property gecko: Boolean; `true` for gecko-based browsers like Firefox.\r\nexport var gecko = userAgentContains('gecko') && !webkit && !opera && !ie;\r\n\r\n// @property safari: Boolean; `true` for the Safari browser.\r\nexport var safari = !chrome && userAgentContains('safari');\r\n\r\nexport var phantom = userAgentContains('phantom');\r\n\r\n// @property opera12: Boolean\r\n// `true` for the Opera browser supporting CSS transforms (version 12 or later).\r\nexport var opera12 = 'OTransition' in style;\r\n\r\n// @property win: Boolean; `true` when the browser is running in a Windows platform\r\nexport var win = navigator.platform.indexOf('Win') === 0;\r\n\r\n// @property ie3d: Boolean; `true` for all Internet Explorer versions supporting CSS transforms.\r\nexport var ie3d = ie && ('transition' in style);\r\n\r\n// @property webkit3d: Boolean; `true` for webkit-based browsers supporting CSS transforms.\r\nexport var webkit3d = ('WebKitCSSMatrix' in window) && ('m11' in new window.WebKitCSSMatrix()) && !android23;\r\n\r\n// @property gecko3d: Boolean; `true` for gecko-based browsers supporting CSS transforms.\r\nexport var gecko3d = 'MozPerspective' in style;\r\n\r\n// @property any3d: Boolean\r\n// `true` for all browsers supporting CSS transforms.\r\nexport var any3d = !window.L_DISABLE_3D && (ie3d || webkit3d || gecko3d) && !opera12 && !phantom;\r\n\r\n// @property mobile: Boolean; `true` for all browsers running in a mobile device.\r\nexport var mobile = typeof orientation !== 'undefined' || userAgentContains('mobile');\r\n\r\n// @property mobileWebkit: Boolean; `true` for all webkit-based browsers in a mobile device.\r\nexport var mobileWebkit = mobile && webkit;\r\n\r\n// @property mobileWebkit3d: Boolean\r\n// `true` for all webkit-based browsers in a mobile device supporting CSS transforms.\r\nexport var mobileWebkit3d = mobile && webkit3d;\r\n\r\n// @property msPointer: Boolean\r\n// `true` for browsers implementing the Microsoft touch events model (notably IE10).\r\nexport var msPointer = !window.PointerEvent && window.MSPointerEvent;\r\n\r\n// @property pointer: Boolean\r\n// `true` for all browsers supporting [pointer events](https://msdn.microsoft.com/en-us/library/dn433244%28v=vs.85%29.aspx).\r\nexport var pointer = !!(window.PointerEvent || msPointer);\r\n\r\n// @property touch: Boolean\r\n// `true` for all browsers supporting [touch events](https://developer.mozilla.org/docs/Web/API/Touch_events).\r\n// This does not necessarily mean that the browser is running in a computer with\r\n// a touchscreen, it only means that the browser is capable of understanding\r\n// touch events.\r\nexport var touch = !window.L_NO_TOUCH && (pointer || 'ontouchstart' in window ||\r\n\t\t(window.DocumentTouch && document instanceof window.DocumentTouch));\r\n\r\n// @property mobileOpera: Boolean; `true` for the Opera browser in a mobile device.\r\nexport var mobileOpera = mobile && opera;\r\n\r\n// @property mobileGecko: Boolean\r\n// `true` for gecko-based browsers running in a mobile device.\r\nexport var mobileGecko = mobile && gecko;\r\n\r\n// @property retina: Boolean\r\n// `true` for browsers on a high-resolution \"retina\" screen or on any screen when browser's display zoom is more than 100%.\r\nexport var retina = (window.devicePixelRatio || (window.screen.deviceXDPI / window.screen.logicalXDPI)) > 1;\r\n\r\n// @property passiveEvents: Boolean\r\n// `true` for browsers that support passive events.\r\nexport var passiveEvents = (function () {\r\n\tvar supportsPassiveOption = false;\r\n\ttry {\r\n\t\tvar opts = Object.defineProperty({}, 'passive', {\r\n\t\t\tget: function () { // eslint-disable-line getter-return\r\n\t\t\t\tsupportsPassiveOption = true;\r\n\t\t\t}\r\n\t\t});\r\n\t\twindow.addEventListener('testPassiveEventSupport', Util.falseFn, opts);\r\n\t\twindow.removeEventListener('testPassiveEventSupport', Util.falseFn, opts);\r\n\t} catch (e) {\r\n\t\t// Errors can safely be ignored since this is only a browser support test.\r\n\t}\r\n\treturn supportsPassiveOption;\r\n}());\r\n\r\n// @property canvas: Boolean\r\n// `true` when the browser supports [`