-
Notifications
You must be signed in to change notification settings - Fork 81
SMap Architecture
Purpose specific: If a JS folder serves a similar purpose to other JS files, they should be grouped together in a subfolder. All form validation files should go inside /parsley
. Any additions to dataTables
should go in the /dataTable
folder.
Vendor Folders: Anything that comes from a third party library. Didn't build it yourself? Copied and pasted from a GitHub repo? Put it in the vendor folder.
There is a vendor folder inside each JS subfolder. If you're working on a specific project, it should have it's own folder, and each third party file you add should go in the vendor file located inside that.
index.js
: Initiates the map, the editor, the router, and the SMap. Contains one event listener that triggers only once when the map has fully loaded.
/dist: this is where the min
files end up after they're compiled by Grunt. These should not be changed manually.
/edit: Everything for adding/editing geometries in leaflet go here. It uses L.Editable.
-
controls.js
: add functionality to the buttons that appear on the left hand side. -
editor.js
: All of the logic that makes adding/editing geometries possible. Augments L.Editable, and includes custom event handlers which can be triggered. Notes inline about what actions trigger what functions and what they do. -
styles.js
: Where all geometry styles are established. If you want to adjust a polygon coloration, this is the place. -
tooltip.js
: Logic/language for the tooltip that appears when you click on one of the toolbar buttons. -
/vendor
:-
LatLngUtils.js
: use for simple replacement/saving of geometry coordinates. -
Leaflet.Editable.js
: main tool for editing geometries. Replaced Leaflet.Draw -
leaflet.toolbar.js
: main repo for the toolbar that appears during editing. -
path.drag.js
: Makes geometries draggable.
-
/map: Everything else needed to make the map work that isn't editing/adding.
-
map.js
: Contains theSMap()
function. The geojsonTileLayer is initiated here, project extents are established, spatial resources are rendered, and any leaflet augmentations need to be initiated here. -
lazytiles.js
: Used to check if a parent tile has already been loaded, in which case, don't load the child tile. -
/vendor:
-
catiline.js
: the web worker used to make tile loading non blocking. Not totally sure this works…. -
L.TileLayer.GeoJSON.js
: All of the logic for loading vector tiles. Could use some improvements (i.e. not loading tiles that have already been zoomed out of, moved past)
-
/router: All of the logic to update the side panel and modal in the project overview.
-
CreateRoutes.js
: Each django view/template, needs it's own url hash, and it's own route. CreateRoutes constructs a dictionary object with each window.location.hash corresponding with a specific set of actions.function route(path, el, controller, eventHook) { routes[path] = { el: el, controller: controller, eventHook: eventHook }; }
-
RouterMixins.js
: Everything but the kitchen sink. State updates, DOM manipulation, Event Hooks, Ajax form submissions, all of that goes here. There are detailed notes aboutupdatePage
andupdateState
inline inRouterMixins.js
-
DOM MANIPULATION
: Anything that affects the physical appears of the page -
UPDATING THE STATE
: Things that affect how the router is able to function. -
ADDING EVENT HOOKS
: Because the html doesn't exist when the JS files initially run, if there are event hooks involved (i.e.$('#delete-location').on('click', function (e) {})
), you have to rerun the JS functions after the router is called. -
INTERCEPTING FORM SUBMISSIONS
: Hooks into any form in a router page and allows them to be submitted via AJAX.- Your submit button needs to have a
submit-btn
class, and aformaction="{{ submit_url }}"
- Your cancel link needs to be
<a hrf="{{ cancel_url }}">
submit_url
should be the async url that handles the form submissioncancel_url
is the hash where you would like to redirect to if a user cancels the form. - Your submit button needs to have a
-
-
SimpleRouter.js
: What happens when the hash changes. Parses and formats that hash to be figure out which router to call in the dictionary created by CreateRoutes, then sends an async request for the django view and the template. Handles any errors/permissions/redirects as well. -
/vendor:
-
L.Hash.js
: adds coordinates to the end of the hash so users can share specific locations.
-
When you load the page, you're loading the ProjectMap view (project_map.html
). Think of this as the skeleton that holds SMap together. Any css/script files you need for ANY of the views need to be loaded here OR loaded dynamically in smap/router/RouterMixins.js
.
If there are django context variables that you need to use in the js files, you'll need to create a global JS variable in project_map.html
. For any other router html file (i.e. location_detail.html
) Django context variables will still be translated, but any JS variables will be loaded as TEXT, not JS, so they won't work. smap.min.js
needs to be loaded after any of these global variable are defined.
<div id="mapid">
contains the leaflet map.
<div id='project-detail'>
: this is the white detail box to the left of the map. It's empty because it's dynamically updated if the hash changes. If you'd like to add a new html page for this detail page, it should only contain the html necessary to fill this box. No wrappers, no extensions, just the barebones html required.
The final new html feature is the additional-modals
div, which is located in project_wrapper.html
. This has the same concept as the project-detail
but it's used for any modal form that appears (i.e. party_add.html
, resource_add.html
, location_delete.html
, etc);
When you're creating a new view, you'll by asked to specify whether it's a "detail" or "modal". This is what it's referring to: Do you want it in the project-detail
div, or the additional-modals
div?
A user arrives at their project page. This calls the ProjectMap view, which calls project_map.html. An event listener in index.js
is established for the window.location.hash
. The project overview is called by default if there is no hash.
The user then clicks on a location. The geometry has a 'url' property that contains the hash for that location (if it is a newly added location, the url will be set in setCurrentLocationFeature
in router/RouterMixin.js
) The window.location.hash
is set to this url property: #/records/location/<location_id>/
Original: /organizations/<org-id>/projects/<proj-id>/records/location/<location-id>/#/records/location/<location_id>/?coords=x/y/z/
Hash: #/records/location/<location_id>/?coords=x/y/z/
The event listener picks up on this, and calls router()
in router/SimpleRouter.js
.
The hash is then parsed to remove any coordinates that were appended automatically (these are not needed for routing).
New Hash: /records/location/<location_id>/
Once the hash is cleaned up, the async url is created by combining async/
+ location.path
+ new_hash
.
Async: async/organizations/<org-id>/projects/<proj-id>/records/location/<location-id>/
router
then checks if this newly formatted hash exists in the routes
dictionary created in router/CreateRoutes.js
. Since routes are generic, if the hash contains an id, it'll return undefined. If the resulting route
variable is null, we then remove the ID.
New New Hash: /records/location/
The resulting route will have four properties:
route['/records/location/'] = {
el: 'detail',
controller: function () {
rm.updatePage({
'page_title': options.trans.location_detail,
'display_detail_panel': true,
'display_modal': false,
});
},
eventHandler: function () {
rm.updateState({
'current_location': window.location.hash,
'datatable': true,
'detach_forms': true,
'active_tab': 'overview',
});
rm.locationDetailHooks();
map.locationEditor.fire('route:location:detail');
});
}
An AJAX call is made to the newly formed async url, which returns an html snippet. route.el tells it which DOM element to update (in this case, it's the detail panel). If there are no permission errors, the route controller is called to make sure all of the appropriate DOM elements are visible (or hidden in the case of the modal). Then the innerHTML is set to the AJAX response.
Once the HTML is updated, the route eventHandler is called. The eventHandler contains all of the actions that aren't possible until the HTML snippet is accessible. From here we can set the current location and establish a link to the geometry for easier navigation, call any javascript files necessary (in this case, call dataTables files, and add eventListeners to all of the resource detach buttons). In this case, the location detail page has its own set of unique hooks that are called after upateState
. This will enable xlang.js
, and add functionality to the tabs for "overview", "relationships" and resources".
If a user then decides to add a relationship, the hash will change to #/records/location/<location-id>/relationships/new/
, which after formatting will return this:
route('/records/location/relationships/new/', 'modal',
function () {
rm.updatePage({
'page_title': options.trans.rel_add,
'display_detail_panel': true,
'display_modal': true,
});
},
function () {
rm.updateState({
'current_location': window.location.hash,
'active_tab': 'relationships',
'form': {
'type': 'modal',
'success_url': 'location',
'tab': 'relationships',
'callback': rm.relationshipAddHooks,
},
});
rm.relationshipAddHooks();
});
The only added feature is the form
property in the eventHandler. This tells any route with a form attached where the form is located (in this case, in the 'modal'), where to redirect to after a successful submission (in this case, the 'location' detail page), and which tab should be active (if applicable, and in this case 'relationships'). The 'callback' property should be the same as the eventHook called after updateState
. If a form submission fails, the HTML is updated, thus removing any eventHooks, so they need to be called again.
When a form is submitted, it's intercepted. The forms formation
contains the async
url (in this case /async/organizations/<org-id>/projects/<proj-id>/records/location/<location-id>/relationships/new/
which is the url the AJAX function calls. If the form fails, the DOM element contain the form's innerHTML is replaced with the response, and the callback is called. If the form is successful, the hash is updated with the success_url
: #/records/location/<location-id>/?tab=relationships/
.
Grunt is currently setup to run whenever you run ./runserver
. It will minify files (run concat
and uglify
), and watch
. It's currently only watching changes inside of SMap, but anytime you change a file it will automatically run concat
and uglify
again, so all you have to do is refresh the page.
Commands:
grunt
: Minifies files and runs JSHint.
grunt runserver
: Minifies files and watches for changes.
grunt production
: Only minifies files.
Visit our User Documentation to learn more about using the Cadasta Platform.
If you'd like to contribute to the Cadasta Platform, start with our Contributing Guidelines.
Cadasta Wiki Home | Developer Setup Guide
Cadasta.org | About Cadasta | YouTube | Twitter | Facebook
- Installing & Running
- Contributing
- Planning & Sprints
- Platform Development
- Testing
- Utilities
- Outreachy
- Platform Site Map
- User Flows and Wireframes
- Other
- Quick Start Guide
- Glossary
- Questionnaire Guide