What we are going to build:
Please review the links from the video, if you have covered other posts on this blog you will know that I'm using grunt, bower, requirejs stack. Below is the source code of the widget:
https://developer.mozilla.org/en-US/d...
https://angularjs.org/
http://angular-ui.github.io/angular-g...
https://developers.google.com/maps/do...
https://angular-ui.github.io/bootstrap/
http://getbootstrap.com/
1. application constants
define(['factories/factories', 'angular'], function (factories, angular) {
'use strict';
factories.factory('ApplicationConstants', ['$window', '$log', function ApplicationConstants($window, $log) {
return {
apiUrl: 'YOUR API URL',
googleApiKey: 'YOUR API KEY'
};
}]);
});
2. utils
define(['factories/factories', 'angular'], function (factories, angular) {
'use strict';
factories.factory('Utils', [ function Utils() {
return {
guid: function () {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
};
}]);
});
3. location service
define(['services/services'], function (services) {
'use strict';
services.factory('LocationService', ['$http', 'ApplicationConstants', '$q', '$log', '$window', '$timeout',
function ($http, ApplicationConstants, $q, $log, $window, $timeout) {
return {
geocodeAddress: function (address) {
var deffered = $q.defer();
var geocoder = new $window.google.maps.Geocoder();
var request = { 'address': address };
//google limits to ~250ms request frequency
$timeout(function () {
geocoder.geocode(request, function (results, status) {
$log.debug('results', results);
$log.debug('status', status);
if (status == 'OK') {
deffered.resolve(results);
} else {
deffered.reject(status);
}
});
}, 300);
return deffered.promise;
},
getCountries: function () {
return $http({
method: 'GET',
url: ApplicationConstants.apiUrl + 'api/Countries',
headers: { 'Content-Type': 'application/json' },
params: {}
});
},
getCities: function (countryId) {
return $http({
method: 'GET',
url: ApplicationConstants.apiUrl + 'api/Cities/Country',
headers: { 'Content-Type': 'application/json' },
params: { 'countryId': countryId }
});
}
};
}]);
});
Please note that to use here the geocoder correctly you have to initialize the google api script as angular-google-maps directives recommend to do in their documentation of getting started.
4. locations directive
define(['directives/directives', 'lodash', 'angular'], function (directives, _, angular) {
'use strict';
directives.directive('locations',
['$log', 'ApplicationConstants', '$timeout', 'Utils', 'uiGmapGoogleMapApi', 'LocationService', 'blockUI', '$window', '$compile',
function ($log, ApplicationConstants, $timeout, Utils, uiGmapGoogleMapApi, LocationService, blockUI, $window, $compile) {
return {
restrict: 'E',
templateUrl: "/public/js/directives/templates/locations.html",
scope: {
locations: '='
},
link: function (scope, element, attr) {
scope.map = undefined;
scope.countries = undefined;
scope.cities = undefined;
scope.selection = {
selectedCountryId: null,
selectedCityId: null,
addressLine: undefined,
locationName: undefined,
locationDescription: undefined,
latitude: undefined,
longitude: undefined
};
scope.$watch('selection', function (newVal, oldVal) {
$log.debug('location selection', newVal);
}, true);
scope.sortOrder = 0;
scope.showAddressLine = false;
scope.showResolveBtn = false;
scope.showAddBtn = false;
scope.showLocationName = false;
scope.showCoordinates = false;
scope.showGeocoderError = false;
scope.showLocationDescription = false;
scope.geocoderMessage = '';
scope.isAdded = false;
scope.accordionStatus = {
isListingOpen: false,
isAddOpen: true,
};
scope.$watch('accordionStatus.isAddOpen', function (newVal, oldVal) {
if (newVal) {
$timeout(function () {
$log.debug('add open')
angular.element('#listingPanel').css('height', '55px').css('overflow', 'auto');
scope.map = { center: { latitude: 33, longitude: -33 }, zoom: 2 };
scope.marker.options.labelContent = '';
scope.marker.options.visible = true;
scope.marker.options.labelVisible = false,
scope.marker = scope.getNewMarker();
for (var i = 0; i < scope.locations.length; ++i) {
scope.locations[i].markerOptions.visible = false;
scope.locations[i].markerWindowOptions.visible = false;
}
scope.showMarker = true;
});
}
});
scope.$watch('accordionStatus.isListingOpen', function (newVal, oldVal) {
if (newVal) {
$timeout(function () {
$log.debug('listing open');
scope.cities = null;
scope.selection.locationName = '';
scope.selection.locationDescription = '';
scope.selection.addressLine = '';
scope.selection.selectedCountryId = null;
scope.selection.selectedCityId = null;
scope.selection.latitude = undefined;
scope.selection.longitude = undefined;
scope.showAddForm = false;
scope.showAddressLine = false;
scope.showResolveBtn = false;
scope.showAddBtn = false;
scope.showLocationName = false;
scope.showLocationDescription = false;
scope.showCoordinates = false;
scope.marker.options.visible = false;
scope.showLocationDescription = false;
scope.marker.options.labelContent = '';
scope.markerWindowTitle = '';
scope.marker.options.labelVisible = false;
angular.element('#listingPanel').css('height', '660px').css('overflow', 'scroll');
for (var i = 0; i < scope.locations.length; ++i) {
scope.locations[i].markerOptions.visible = true;
scope.locations[i].markerOptions.labelVisible = true;
scope.locations[i].markerWindowOptions.visible = true;
}
if (!scope.mapControl) {
scope.mapControl = {};
}
$log.debug('locations:', scope.locations);
scope.centerMapToMarkers();
});
} else {
$timeout(function () {
$log.debug('listing close');
angular.element('#listingPanel').css('height', '60px').css('overflow', 'auto');
});
}
});
scope.currentZoom = 2;
scope.zoomOut = function () {
scope.mapControl.getGMap().setZoom(scope.mapControl.getGMap().getZoom() - 1);
}
scope.centerMapToMarkers = function () {
$timeout(function () {
$log.debug('center to markers');
var map = scope.mapControl.getGMap();
var markerControls = _.keys(scope.mapControl);
$log.debug('map', map);
$log.debug('marker controls', markerControls);
var markers = [];
$log.debug('map ctrl:', scope.mapControl);
for (var i = 0; i < markerControls.length; ++i) {
$log.debug(i, markerControls[i]);
$log.debug(i, scope.mapControl[markerControls[i]]['getGMarkers']);
if (markerControls[i] !== 'current' && markerControls[i] !== 'refresh' && !angular.isFunction(scope.mapControl[markerControls[i]])) {
markers.push(scope.mapControl[markerControls[i]]['getGMarkers']()[0]);
}
}
$log.debug('markers', markers)
map.setZoom(16);
var bounds = new $window.google.maps.LatLngBounds();
$log.debug(bounds);
for (var i = 0; i < markers.length; ++i) {
bounds.extend(markers[i].getPosition());
markers[i].setAnimation(-1);
}
map.setCenter(bounds.getCenter());
map.fitBounds(bounds);
map.setZoom(map.getZoom() - 1);
});
};
scope.mapControl = { 'current': {} };
scope.getNewMarker = function () {
scope.markerId = Utils.guid();
return {
id: scope.markerId,
coordinates: {
latitude: 33,
longitude: -33
},
options: {
animation: 1,
draggable: true,
icon: '/public/js/images/doctor_icon_new.png',
visible: true,
labelContent: '',
labelAnchor: "33 0",
labelClass: "marker-labels",
labelVisible: false,
},
events: {
position_changed: function (marker, eventName, model, args) {
$timeout(function () {
$log.debug('positionChanged');
$log.debug(marker.getPosition());
scope.selection.latitude = $window.Math.round(marker.getPosition().lat() * 100000) / 100000;
scope.selection.longitude = $window.Math.round(marker.getPosition().lng() * 100000) / 100000;
$log.debug(marker);
$log.debug(eventName),
$log.debug(model);
$log.debug(args);
marker.setAnimation(1);
});
},
dragend: function (marker, eventName, model, args) {
$timeout(function () {
marker.setAnimation(1);
$log.debug('marker dragend');
$log.debug(eventName);
$log.debug(model);
$log.debug(args);
var latitude = $window.Math.round(marker.getPosition().lat() * 100000) / 100000;
var longitude = $window.Math.round(marker.getPosition().lng() * 100000) / 100000;
$log.debug(latitude);
$log.debug(longitude);
scope.selection.latitude = latitude;
scope.selection.longitude = longitude;
scope.marker.options = {
icon: '/public/js/images/doctor_icon_new.png',
draggable: true,
labelContent: scope.selection.locationName,
labelAnchor: "33 0",
labelClass: "marker-labels",
visible: true
};
});
}
}
};
};
scope.markerId = undefined;
scope.showMarker = false;
scope.marker = scope.getNewMarker();
scope.markerWindowOptions = {
visible: true
};
scope.onMarkerClick = function () {
scope.markerWindowOptions.visible = !scope.markerWindowOptions.visible;
};
scope.closeMarkerWindow = function (locationId) {
if (!locationId) {
scope.markerWindowOptions.visible = false;
} else {
scope.locations[_.findIndex(scope.locations, function (location) { return location.id === locationId })].markerWindowOptions.visible = false;
}
};
scope.markerWindowTitle = '';
scope.$watchCollection('locations', function (newVal, oldVal) {
$timeout(function () {
$log.debug('locations', newVal);
if (newVal && newVal.length > 0 && scope.isAdded) {
scope.accordionStatus.isListingOpen = true;
} else {
scope.accordionStatus.isListingOpen = false;
}
});
});
scope.resolve = function () {
$timeout(function () {
var address = scope.countries[_.findIndex(scope.countries, function (country) { return country.id === scope.selection.selectedCountryId; })].name
+ " " +
scope.cities[_.findIndex(scope.cities, function (city) { return city.id === scope.selection.selectedCityId })].name
+ " " +
scope.selection.addressLine;
$log.debug('address', address);
blockUI.start('Resolving location ...')
LocationService.geocodeAddress(address).then(function (geocodeResult) {
$log.debug('geocoder data', geocodeResult);
scope.marker.options.visible = true;
scope.showGeocoderError = false;
scope.geocoderMessage = '';
scope.showLocationName = true;
scope.showLocationDescription = true;
scope.showMarker = true;
scope.map.center.latitude = scope.selection.latitude = scope.marker.coordinates.latitude = $window.Math.round(geocodeResult[0].geometry.location.k * 100000) / 100000;
scope.map.center.longitude = scope.selection.longitude = scope.marker.coordinates.longitude = $window.Math.round(geocodeResult[0].geometry.location.D * 100000) / 100000;
scope.map.zoom = 16;
blockUI.stop();
}, function (status) {
switch (status) {
case 'ZERO_RESULTS':
scope.showGeocoderError = true;
scope.geocoderMessage = 'search returned zero results, please try again different search parameters.';
break;
case 'OVER_QUERY_LIMIT':
scope.showGeocoderError = true;
scope.geocoderMessage = 'search query limit reached, please try again later...';
break;
case 'REQUEST_DENIED':
scope.showGeocoderError = true;
scope.geocoderMessage = 'search was denied, please try again different search parameters.';
break;
}
blockUI.stop();
$log.debug('geocoder error', error);
}
);
}, 300);
};
scope.selectLocation = function (locationId) {
$timeout(function () {
$log.debug('mapControl', scope.mapControl);
var markerControls = _.keys(scope.mapControl);
var markers = [];
for (var i = 0; i < markerControls.length; ++i) {
$log.debug(i, markerControls[i]);
if (markerControls[i] !== 'current' && markerControls[i] !== 'refresh' && !angular.isFunction(scope.mapControl[markerControls[i]])) {
markers.push(scope.mapControl[markerControls[i]]['getGMarkers']()[0]);
}
}
var location = scope.locations[_.findIndex(scope.locations, function (loc) { return loc.id === locationId })];
$log.debug('markers', markers);
$log.debug('location', location);
var indexToSelect = -1;
var currentMarkerPosition = null
for (var i = 0; i < markers.length; ++i) {
markers[i].setAnimation(-1);
markers[i].setIcon('/public/js/images/doctor_icon_added.png');
currentMarkerPosition = markers[i].getPosition();
$log.debug('marker position', currentMarkerPosition);
$log.debug('location position', location.coordinates);
$log.debug('test1', $window.Math.round(currentMarkerPosition.lng() * 100000) / 100000, location.coordinates.longitude)
$log.debug('test2', $window.Math.round(currentMarkerPosition.lat() * 100000) / 100000, location.coordinates.latitude)
if ($window.Math.round(currentMarkerPosition.lng() * 100000) / 100000 === location.coordinates.longitude
&&
$window.Math.round(currentMarkerPosition.lat() * 100000) / 100000 === location.coordinates.latitude
) {
indexToSelect = i;
}
}
$log.debug(indexToSelect);
markers[indexToSelect].setAnimation(1);
markers[indexToSelect].setIcon('/public/js/images/doctor_icon_new.png');
scope.map = { center: { latitude: markers[indexToSelect].getPosition().lat(), longitude: markers[indexToSelect].getPosition().lng() }, zoom: scope.mapControl.getGMap().getZoom() + 1 };
});
};
scope.deleteLocation = function (locationId) {
if (confirm('Are you sure that you want to delete this location ?')) {
_.remove(scope.locations, function (location) { return location.id === locationId });
scope.centerMapToMarkers();
}
};
scope.add = function () {
$timeout(function () {
if (!scope.mapControl) {
scope.mapControl = {};
}
scope.mapControl[scope.markerId] = {}
scope.isAdded = true;
$log.debug('new location', location);
if (scope.locations.length === undefined) {
scope.locations = [];
}
scope.locations.push({
id: scope.markerId,
countryId: scope.countries[_.findIndex(scope.countries, function (country) { return country.id === scope.selection.selectedCountryId })].id,
cityId: scope.cities[_.findIndex(scope.cities, function (city) { return city.id === scope.selection.selectedCityId })].id,
address: scope.selection.addressLine,
name: scope.selection.locationName,
description: scope.selection.locationDescription,
coordinates: {
latitude: _.clone(scope.selection.latitude),
longitude: _.clone(scope.selection.longitude)
},
markerOptions: {
animation: -1,
draggable: false,
icon: '/public/js/images/doctor_icon_added.png',
visible: true,
labelContent: scope.selection.locationName,
labelAnchor: "33 0",
labelClass: "marker-labels",
labelVisible: true,
},
markerWindowOptions: {
visible: true
},
sortOrder: scope.sortOrder++
});
scope.accordionStatus.isAddOpen = false;
scope.accordionStatus.isListingOpen = true;
});
};
scope.locationDescriptionChange = function () {
$timeout(function () {
scope.markerWindowTitle = '';
if (scope.selection.locationDescription !== '') {
scope.markerWindowTitle = scope.selection.locationDescription;
scope.markerWindowOptions.visible = true;
} else {
scope.showAddBtn = false;
}
if (scope.selection.locationName !== '' && scope.selection.locationDescription !== '') {
scope.showAddBtn = true;
}
});
};
scope.locationNameChange = function () {
$timeout(function () {
scope.marker.options.labelContent = '';
scope.marker.options.labelVisible = false;
$log.debug(scope.selection.locationName);
if (scope.selection.locationName !== '') {
scope.marker.options.labelVisible = true;
scope.marker.options.labelContent = scope.selection.locationName;
} else {
scope.showAddBtn = false;
scope.marker.options.labelContent = '';
scope.marker.options.labelVisible = false;
}
if (scope.selection.locationName !== '' && scope.selection.locationDescription !== '') {
scope.showAddBtn = true;
}
});
};
scope.addressLineChange = function () {
$timeout(function () {
if (scope.selection.addressLine !== '') {
scope.showResolveBtn = true;
scope.showCoordinates = true;
scope.showGeocoderError = false;
scope.geocoderMessage = '';
} else {
scope.selection.locationName = '';
scope.selection.locationDescription = '';
scope.selection.latitude = '';
scope.selection.longitude = '';
scope.showResolveBtn = false;
scope.showCoordinates = false;
scope.showAddBtn = false;
scope.showLocationName = false;
scope.showLocationDescription = false;
scope.showGeocoderError = false;
scope.geocoderMessage = '';
scope.showMarker = false;
}
});
};
scope.cityChange = function () {
$timeout(function () {
if (scope.selection.selectedCityId) {
scope.showAddressLine = true;
scope.showResolveBtn = false;
scope.selection.addressLine = undefined;
scope.showGeocoderError = false;
scope.geocoderMessage = '';
} else {
scope.selection.locationName = '';
scope.selection.locationDescription = '';
scope.selection.addressLine = '';
scope.selection.latitude = '';
scope.selection.longitude = '';
scope.showAddForm = false;
scope.showAddressLine = false;
scope.showResolveBtn = false;
scope.showCoordinates = false;
scope.showAddBtn = false;
scope.showLocationName = false;
scope.showLocationDescription = false;
scope.showGeocoderError = false;
scope.geocoderMessage = '';
scope.showMarker = false;
}
});
};
scope.countryChange = function () {
$timeout(function () {
if (scope.selection.selectedCountryId) {
blockUI.start('Loading Cities For ' + scope.countries[_.findIndex(scope.countries, function (country) { return country.id === scope.selection.selectedCountryId; })].name + ' ...');
LocationService.getCities(scope.selection.selectedCountryId).then(function (citiesData) {
$timeout(function () {
$log.debug(citiesData.data);
scope.cities = citiesData.data;
scope.showGeocoderError = false;
scope.geocoderMessage = '';
blockUI.stop();
});
}, function (error) {
blockUI.stop();
$log.debug(error);
})
} else {
scope.showGeocoderError = false;
scope.geocoderMessage = '';
scope.showLocationDescription = false;
scope.selection.locationName = '';
scope.selection.locationDescription = '';
scope.selection.addressLine = '';
scope.selection.latitude = '';
scope.selection.longitude = '';
scope.selection.selectedCityId = null;
scope.showAddForm = false;
scope.showAddressLine = false;
scope.showResolveBtn = false;
scope.showCoordinates = false;
scope.showAddBtn = false;
scope.showLocationName = false;
scope.showMarker = false;
}
});
};
uiGmapGoogleMapApi.then(function (maps) {
$log.debug('maps ready', maps);
$timeout(function () {
scope.map = { center: { latitude: 33, longitude: -33 }, zoom: 2 };
//scope.mapControl['current'] = {};
blockUI.start('Loading Countries ...');
LocationService.getCountries().then(function (countriesData) {
$log.debug('Countries', countriesData.data);
$timeout(function () {
scope.countries = countriesData.data;
//scope.mapControl = {};
scope.sortOrder = 0;
if (scope.locations && scope.locations.length > 0) {
for (var i = 0; i < scope.locations.length; ++i) {
$log.debug('marker options', scope.locations[i].markerOptions);
scope.locations[i].markerOptions.visible = false;
scope.locations[i].markerOptions.labelVisible = false;
scope.locations[i].markerWindowOptions.visible = false;
scope.mapControl[scope.locations[i].id] = {};
++scope.sortOrder;
$compile(angular.element('#' + scope.locations[i].id).contents())(scope);
}
}
scope.marker = scope.getNewMarker();
scope.isAdded = false;
scope.accordionStatus.isAddOpen = true;
scope.accordionStatus.isListingOpen = false;
});
blockUI.stop();
}, function (error) {
blockUI.stop();
$log.debug(error);
});
});
});
}
};
}]);
});
5. location directive template
<div class="container-fluid" id="locationsWrapper">
<div class="col-md-8">
<ui-gmap-google-map center='map.center' zoom='map.zoom' control="mapControl">
<ui-gmap-marker coords="marker.coordinates" options="marker.options" events="marker.events" idkey="marker.id" control="mapControl['current']">
<ui-gmap-window options="markerWindowOptions" closeclick="closeMarkerWindow()" show="showLocationDescription">
<div>{{markerWindowTitle}}</div>
</ui-gmap-window>
</ui-gmap-marker>
<ui-gmap-marker ng-repeat="location in locations" coords="location.coordinates" options="location.markerOptions" idkey="location.id" control="mapControl[location.id]">
<ui-gmap-window options="location.markerWindowOptions" closeclick="closeMarkerWindow(location.id)" show="location.markerWindowOptions.visible">
<div>{{location.description}}</div>
</ui-gmap-window>
</ui-gmap-marker>
</ui-gmap-google-map>
</div>
<div class="col-md-4">
<accordion close-others="true">
<accordion-group is-open="accordionStatus.isAddOpen">
<accordion-heading>
<div class="noselect"><span class="glyphicon glyphicon-plus" style="margin-right:5px;"></span><span>Add Location</span><i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': accordionStatus.isAddOpen, 'glyphicon-chevron-right': !accordionStatus.isAddOpen}"></i></div>
</accordion-heading>
<div id="addLocationFormWrapper" class="container-fluid">
<ng-form name="addLocationForm">
<div class="form-group">
<label class="text-left" for="country" name="lblCountry" id="lblCountry">Country</label>
<select name="country" id="country" class="form-control text-left" ng-model="selection.selectedCountryId" required ng-options="country.id as country.name for country in countries" ng-disabled="countries == null" ng-change="countryChange()" tooltip="Select country" tooltip-placement="bottom" tooltip-class="default">
<option value="">--Select--</option>
</select>
</div>
<div class="form-group">
<label class="text-left" for="city" name="lblCity" id="lblCity">City</label>
<select name="city" id="city" class="form-control text-left" ng-model="selection.selectedCityId" required ng-options="city.id as city.name for city in cities" ng-disabled="selection.selectedCountryId == null" ng-change="cityChange()" tooltip="Select city" tooltip-placement="bottom" tooltip-class="default">
<option value="">--Select--</option>
</select>
</div>
<div class="form-group" ng-show="showAddressLine">
<label name="lblAddressLine" id="lblAddressLine" class="control-label text-left" for="addressLine">Street, Building's Number and Postcode</label>
<textarea name="addressLine" class="form-control text-left" id="addressLine" ng-model="selection.addressLine" ng-change="addressLineChange()" tooltip="Provide address" tooltip-placement="bottom" tooltip-class="default"></textarea>
</div>
<div class="form-group" ng-show="showResolveBtn">
<div class="alert alert-danger alert-dismissible alert-dismissible" role="alert" ng-show="showGeocoderError"><span class="glyphicon glyphicon-info-sign"></span> Address couldn't be resolved due to: {{geocoderMessage}}</div>
<button name="btnLocalizeOnMap" id="btnLocalizeOnMap" type="button" class="btn btn-primary text-left" ng-click="resolve()" tooltip="Click to localize on the map" tooltip-placement="bottom" tooltip-class="default">Resolve</button>
</div>
<div class="form-group" ng-show="showCoordinates">
<div class="col-md-12"><span class="help-text">Please drag the marker to change the coordinates.</span></div>
<div class="col-md-12">
<label class="text-left" for="latitude" name="lblLatitude" id="lblLatitude">Latitude</label>
<input class="form-control input-sm" name="latitude" id="latitude" ng-model="selection.latitude" ng-disabled="true" style="width:80px" tooltip="Latitude" tooltip-placement="bottom" tooltip-class="default" />
<label class="text-left" for="longitude" name="lblLongitude" id="lblLongitude">Longitude</label>
<input class="form-control input-sm" name="longitude" id="longitude" ng-model="selection.longitude" ng-disabled="true" style="width:80px;" tooltip="Longitude" tooltip-placement="bottom" tooltip-class="default" />
</div>
</div>
<div class="form-group" ng-show="showLocationName" style="margin-top: 10px;">
<label name="lblLocationName" id="lblLocationName" class="control-label text-left" for="locationName">Location's Name</label>
<textarea name="locationName" class="form-control text-left" id="locationName" ng-model="selection.locationName" ng-change="locationNameChange()" tooltip="Your name of the location" tooltip-placement="bottom" tooltip-class="default"></textarea>
</div>
<div class="form-group" ng-show="showLocationName">
<label name="lblLocationDescription" id="lblLocationDescription" class="control-label text-left" for="locationDescription">Location's Description</label>
<textarea name="locationDescription" class="form-control text-left" id="locationDescription" ng-model="selection.locationDescription" ng-change="locationDescriptionChange()" tooltip="Your description of location" tooltip-placement="bottom" tooltip-class="default"></textarea>
</div>
<div class="form-group" ng-show="showAddBtn">
<button name="btnAddLocation" id="btnAddLocation" type="button" class="btn btn-success text-left" ng-click="add()" tooltip="Add location to current locations" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-plus"></span> Add</button>
</div>
</ng-form>
</div>
</accordion-group>
<accordion-group is-open="accordionStatus.isListingOpen" style="height:55px;overflow:auto;" id="listingPanel">
<accordion-heading>
<div class="noselect" ng-click="alert('showall')"><span class="glyphicon glyphicon-globe" style="margin-right:5px;"></span><span>Current Locations</span><i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': accordionStatus.isListingOpen, 'glyphicon-chevron-right': !accordionStatus.isListingOpen}"></i></div>
</accordion-heading>
<div class="container-fluid">
<div ng-repeat="location in locations">
<div class="panel panel-default">
<div class="panel-heading">
<div class="row">
<div class="col-md-12">
<span class="glyphicon glyphicon-globe"></span> {{location.name}}
</div>
</div>
</div>
<div class="panel-body">
<div class="row">
<div class="col-md-12">{{location.description}}</div>
</div>
<div class="row"> </div>
<div class="row">
<div class="col-md-7">
<div class="btn-group-vertical" role="group">
<button type="button" class="btn-primary btn-sm" ng-click="selectLocation(location.id)" tooltip="Zoom in on location" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-search"></span> <span class="glyphicon glyphicon-plus-sign"></span></button>
<button type="button" class="btn-primary btn-sm" ng-click="zoomOut()" tooltip="Zoom out on location" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-search"></span> <span class="glyphicon glyphicon-minus-sign"></span></button>
</div>
</div>
<div class="col-md-5">
<button type="button" class="btn-danger btn-sm" ng-click="deleteLocation(location.id)" tooltip="delete location" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-remove"></span></button>
</div>
</div>
</div>
<div class="panel-footer">
<div class="row">
<div class="col-md-12"><small>{{location.address}}</small></div>
</div>
<div class="row">
<div class="col-md-12"><small>{{location.city.name}} {{location.country.name}}</small></div>
</div>
</div>
</div>
<br />
</div>
</div>
</accordion-group>
</accordion>
</div>
</div>
6. C# WebAPI Controllers
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using API.DomainModel;
namespace API.Controllers
{
[RoutePrefix("api/Coutries")]
public class CountriesController : ApiController
{
private Entities db = new Entities();
[HttpGet]
[AllowAnonymous]
[ResponseType(typeof(IQueryable<Country>))]
public async Task<IHttpActionResult> GetCountries()
{
return Ok(await db.Countries.Select(c => new { c.Id, c.Name} ).ToListAsync());
}
// GET: api/Countries/5
[HttpGet]
[Authorize(Roles = "admin")]
[ResponseType(typeof(Country))]
public async Task<IHttpActionResult> GetCountry(Guid id)
{
Country country = await db.Countries.FindAsync(id);
if (country == null)
{
return NotFound();
}
return Ok(country);
}
// PUT: api/Countries/5
[HttpPut]
[Authorize(Roles = "admin")]
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutCountry(Guid id, Country country)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != country.Id)
{
return BadRequest();
}
db.Entry(country).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CountryExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Countries
[HttpPost]
[Authorize(Roles = "admin")]
[ResponseType(typeof(Country))]
public async Task<IHttpActionResult> PostCountry(Country country)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Countries.Add(country);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (CountryExists(country.Id))
{
return Conflict();
}
else
{
throw;
}
}
return CreatedAtRoute("DefaultApi", new { id = country.Id }, country);
}
// DELETE: api/Countries/5
[HttpDelete]
[Authorize(Roles = "admin")]
[ResponseType(typeof(Country))]
public async Task<IHttpActionResult> DeleteCountry(Guid id)
{
Country country = await db.Countries.FindAsync(id);
if (country == null)
{
return NotFound();
}
db.Countries.Remove(country);
await db.SaveChangesAsync();
return Ok(country);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool CountryExists(Guid id)
{
return db.Countries.Count(e => e.Id == id) > 0;
}
}
}
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Description;
using API.DomainModel;
namespace API.Controllers
{
[RoutePrefix("api/Cities")]
public class CitiesController : ApiController
{
private Entities db = new Entities();
//GET: api/Cities/Country/1
[HttpGet]
[Route("Country")]
[AllowAnonymous]
[ResponseType(typeof(IQueryable<City>))]
public async Task<IHttpActionResult> GetCitiesByCountry(Guid countryId)
{
return Ok(await db.Cities.Where(c => c.CountryId == countryId).Select(city => new { city.Id, city.CountryId, city.Name }).ToListAsync());
}
// GET: api/Cities
[HttpGet]
[Authorize(Roles="admin")]
[ResponseType(typeof(IQueryable<City>))]
public async Task<IHttpActionResult> GetCities()
{
return Ok(await db.Cities.Select(city => new { city.Id, city.CountryId, city.Name}).ToListAsync());
}
// GET: api/Cities/5
[HttpGet]
[Authorize(Roles="admin")]
[ResponseType(typeof(City))]
public async Task<IHttpActionResult> GetCity(Guid id)
{
City city = await db.Cities.FindAsync(id);
if (city == null)
{
return NotFound();
}
return Ok(city);
}
// PUT: api/Cities/5
[HttpPut]
[Authorize(Roles = "admin")]
[ResponseType(typeof(void))]
public async Task<IHttpActionResult> PutCity(Guid id, City city)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (id != city.Id)
{
return BadRequest();
}
db.Entry(city).State = EntityState.Modified;
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!CityExists(id))
{
return NotFound();
}
else
{
throw;
}
}
return StatusCode(HttpStatusCode.NoContent);
}
// POST: api/Cities
[HttpPost]
[Authorize(Roles = "admin")]
[ResponseType(typeof(City))]
public async Task<IHttpActionResult> PostCity(City city)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.Cities.Add(city);
try
{
await db.SaveChangesAsync();
}
catch (DbUpdateException)
{
if (CityExists(city.Id))
{
return Conflict();
}
else
{
throw;
}
}
return CreatedAtRoute("DefaultApi", new { id = city.Id }, city);
}
// DELETE: api/Cities/5
[HttpDelete]
[Authorize(Roles = "admin")]
[ResponseType(typeof(City))]
public async Task<IHttpActionResult> DeleteCity(Guid id)
{
City city = await db.Cities.FindAsync(id);
if (city == null)
{
return NotFound();
}
db.Cities.Remove(city);
await db.SaveChangesAsync();
return Ok(city);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
private bool CityExists(Guid id)
{
return db.Cities.Count(e => e.Id == id) > 0;
}
}
}
7. C# Entity Framework
namespace API.DomainModel
{
using System;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
public partial class Entities : DbContext
{
public Entities()
: base("name=Entities")
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
throw new UnintentionalCodeFirstException();
}
public virtual DbSet<City> Cities { get; set; }
public virtual DbSet<Country> Countries { get; set; }
}
}
namespace API.DomainModel
{
using System;
using System.Collections.Generic;
public partial class City
{
public City()
{
}
public System.Guid Id { get; set; }
public Nullable<System.Guid> CountryId { get; set; }
public string Name { get; set; }
public virtual Country Country { get; set; }
}
}
namespace API.DomainModel
{
using System;
using System.Collections.Generic;
public partial class Country
{
public Country()
{
this.Cities = new HashSet<City>();
}
public System.Guid Id { get; set; }
public string Name { get; set; }
public virtual ICollection<City> Cities { get; set; }
}
}
7. C# WebApi Config Related
using API.DomainModel;
using API.Identity;
using API.Providers;
using Microsoft.Owin;
using Owin;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
using System.Web.Http;
[assembly: OwinStartup(typeof(API.Startup))]
namespace API
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
config.Formatters.Add(new BrowserJsonFormatter());
WebApiConfig.Register(config);
app.UseWebApi(config);
}
}
}
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Formatting;
using System.Net.Http.Headers;
using System.Web;
namespace API
{
public class BrowserJsonFormatter : JsonMediaTypeFormatter
{
public BrowserJsonFormatter()
{
this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));
this.SerializerSettings.Formatting = Formatting.Indented;
}
public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType)
{
base.SetDefaultContentHeaders(type, headers, mediaType);
headers.ContentType = new MediaTypeHeaderValue("application/json");
}
}
}
8. Database SQL
CREATE TABLE [dbo].[Countries](
[Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Countries_Id] DEFAULT (newid()),
[Name] [nvarchar](50) NOT NULL,
CONSTRAINT [PK_Countries] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
CREATE TABLE [dbo].[Cities](
[Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Cities_Id] DEFAULT (newid()),
[CountryId] [uniqueidentifier] NULL,
[Name] [nvarchar](255) NULL,
CONSTRAINT [PK_Cities] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
ALTER TABLE [dbo].[Cities] WITH CHECK ADD CONSTRAINT [FK_Cities_Countries] FOREIGN KEY([CountryId])
REFERENCES [dbo].[Countries] ([Id])
ON UPDATE CASCADE
ON DELETE CASCADE
ALTER TABLE [dbo].[Cities] CHECK CONSTRAINT [FK_Cities_Countries]
This is I believe all what is required (transfered minimal code from my project) besides entering the entries to the database. If any questions then feel free to comment. I hope someone will find this code somehow useful. Here is the usage of the directive somewhere in your system:
<locations locations="myLocations"></locations>