Модуль Роскосмос предназначен для отображения на карте снимков земной поверхности, полученных аппаратами Метеор-М1, Канопус-МСС, Канопус-ПСС, Ресурс-ДК1 и Алос.
Поскольку доступ к изображениям со всех этих спутников, кроме Метеор-М1, предоставляется только зарегистрированным пользователям портала Роскосмос, необходимо создать «прокси-сервер», передающий сервису Роскосмоса регистрационные данные, получающий запрошенные снимки и возвращающий их клиенту (в браузер).
Клиентская (браузерная) часть, очевидно, должна давать авторизованному пользователю возможность выбрать какие снимки с каких спутников он хочет видеть, загрузить выбранные изображения и разместить их на карте. Значит, после того как пользователь вошёл в систему, в правой панели должна появляться дополнительная вкладка, на которой можно выбрать спутник и увидеть список снимков с этого спутника, и в списке отметить интересующие изображения. Для большего удобства изображения со спутника группируются по году и месяцу съёмки. Кроме того, предусмотрены инструменты, позволяющие просмотреть информацию о снимке в развёрнутом виде и перейти к изображению непосредственно на карте.
Серверная часть
Cерверная часть организована в виде пяти контроллеров, каждый из которых отвечает за свой спутник. Все контроллеры реализуют одинаковый набор экшенов:
list — для получения списка снимков за указанный параметром год или за все годы;
map — для получения отдельного снимка;
years — вспомогательный экшен, возвращающий список годов, для которых доступны снимки, т.к. разные спутники функционируют разное время.
Поскольку доступ к данным с разных спутников происходит одинаково, различаясь только URL-адресом на портале Роскосмоса, эта функциональность была вынесена в отдельный класс Satellite, используемый в классах-контроллерах.
public class Satellite {
private static final String URL_PATTERN = "http://gptl.ru/wms/%s?SERVICE=WMS&REQUEST=GetCapabilities&username=%s&md5password=%s";
private static final int EXPIRETY = 60*60*24; // 24 hours in seconds
private static JsonNodeFactory nc = JsonNodeFactory.instance;
private static String gptlLogin = Play.application().configuration().getString("application.gptl.login");
private static String md5hash;
static {
try {
byte[] pass = Play.application().configuration().getString("application.gptl.password").getBytes("UTF-8");
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(pass);
BigInteger bigInt = new BigInteger(1,digest);
md5hash = bigInt.toString(16);
} catch (UnsupportedEncodingException e) {
// shouldn't ever happen
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
// shouldn't ever happen
e.printStackTrace();
}
}
private String name;
private String title;
private String wmsURL;
private WebMapServer wms;
private List<Layer> layerList;
public Satellite(String title, String wmsURL) {
this.title = title;
this.name = wmsURL;
this.wmsURL = String.format(URL_PATTERN, wmsURL, gptlLogin, md5hash);
try {
URL url = new URL(this.wmsURL);
wms = new WebMapServer(url);
} catch (IOException e) {
//e.printStackTrace();
// FIXME: do nothing for now
} catch (ServiceException e) {
//e.printStackTrace();
// FIXME: do nothing for now
}
}
public JsonNode years() throws WMSUninitializedException {
ensureWMS();
ensureLayerList();
return stringListToJSON(collectYears(layerList));
}
public JsonNode list() throws WMSUninitializedException {
ensureWMS();
ensureLayerList();
return layersToJSON(layerList);
}
public JsonNode list(String year) throws WMSUninitializedException {
ensureWMS();
ensureLayerList();
return layersToJSON(layersForYear(layerList, year));
}
public InputStream map(String layerName, String format, String version,
String width, String height, String srs, String bbox) throws WMSUninitializedException {
ensureWMS();
ensureLayerList();
Layer layer = getLayerByName(layerName);
GetMapRequest request = wms.createGetMapRequest();
request.addLayer(layer);
request.setVersion(version);
request.setFormat(format);
request.setDimensions(width, height); //sets the dimensions to be returned from the server
request.setTransparent(true);
request.setSRS(srs);
request.setBBox(bbox);
Logger.info("Satellite.map: " + request.getFinalURL());
try {
GetMapResponse response = wms.issueRequest(request);
return response.getInputStream();
} catch (IOException e) {
//e.printStackTrace();
// FIXME: do nothing for now
} catch (ServiceException e) {
//e.printStackTrace();
// FIXME: do nothing for now
}
return null;
}
/**
* Pre-condition: ensureLayerList()
* @param name layer's name
* @return @code{Layer} object for given name or @code{null} for wrong name
*/
private Layer getLayerByName(String name) {
for (Layer layer : layerList) {
if (layer.getName().equals(name))
return layer;
}
return null;
}
private synchronized void ensureWMS() throws WMSUninitializedException {
if (wms == null)
throw new WMSUninitializedException();
}
private synchronized void ensureLayerList() {
if (layerList == null) {
layerList = (List<Layer>)Cache.get("layers." + this.name);
if (layerList == null) {
WMSCapabilities capabilities = wms.getCapabilities();
layerList = capabilities.getLayerList();
layerList = layerList.subList(1, layerList.size());
final List<Layer> tmp = new ArrayList<Layer>(layerList.size());
tmp.addAll(layerList);
Collections.sort(tmp, new Comparator<Layer>() {
public int compare(Layer l1, Layer l2) {
final String name1 = l1.getName();
final String date1 = name1.split("_")[1];
final String name2 = l2.getName();
final String date2 = name2.split("_")[1];
return -name1.compareTo(name2);
}
});
layerList = Collections.unmodifiableList(tmp);
Cache.set("layers." + this.name, layerList, EXPIRETY);
}
}
}
private static List<Layer> layersForYear(List<Layer> layersList, String year) {
List<Layer> filtered = new ArrayList<Layer>();
for (Layer l : layersList) {
if (yearFromLayer(l).equals(year)) {
filtered.add(l);
}
}
return filtered;
}
private JsonNode layersToJSON(List<Layer> layersList) {
String today = dateFromString("");
ArrayNode arr = new ArrayNode(nc);
for (Layer l : layersList) {
ObjectNode n = layerToJson(l);
String date = n.get("date").getTextValue();
if (date.compareTo(today) <= 0)
arr.add(n);
}
return arr;
}
private ObjectNode layerToJson(Layer layer) {
String name = layer.getName();
String[] elems = name.split("_");
CRSEnvelope boundingBox = layer.getLatLonBoundingBox();
ObjectNode bbox = new ObjectNode(nc);
bbox.put("minx", boundingBox.getMinX());
bbox.put("miny", boundingBox.getMinY());
bbox.put("maxx", boundingBox.getMaxX());
bbox.put("maxy", boundingBox.getMaxY());
ObjectNode json = new ObjectNode(nc);
json.put("name", name);
json.put("satelliteTitle", this.title);
json.put("satellite", this.name.toLowerCase());
json.put("date", dateFromString(elems[1]));
json.put("bbox", bbox);
return json;
}
private JsonNode stringListToJSON(Collection<String> stringList) {
ArrayNode arr = new ArrayNode(nc);
for (String s : stringList)
arr.add(s);
return arr;
}
private static Collection<String> collectYears(List<Layer> layersList) {
Calendar cal = Calendar.getInstance();
String thisYear = String.valueOf(cal.get(Calendar.YEAR));
List<String> years = new ArrayList<String>();
for (Layer l : layersList) {
String year = yearFromLayer(l);
if (!years.contains(year) && (year.compareTo(thisYear) <= 0)) {
years.add(year);
}
}
return years;
}
private static String yearFromLayer(Layer layer) {
final String name = layer.getName();
final String date = dateFromString(name.split("_")[1]);
final String year = date.split("-")[0];
return year;
}
private static String dateFromString(String maybeDate) {
final String[] parts = maybeDate.split("-");
if (parts.length == 3) {
return maybeDate;
}
final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
return df.format(new Date());
}
}
Для получения списка доступных изображений и самого изображения (по протоколу WMS) используется библиотека GeoTools. Кроме того, поскольку список доступных снимков меняется нечасто, он помещается в кеш для более быстрого доступа при последующих запросах.
Однако, поскольку экшены в Play! Framework являются статическими методами, которые не могут наследоваться, код этих методов дублируется во всех контроллерах.
public class MeteorM1 extends Abstract {
private static Satellite satellite = new Satellite("Метеор-М1", "Meteor-M1");
public static Result years() {
Promise<JsonNode> promise = Akka.future(
new Callable<JsonNode>() {
public JsonNode call() {
try {
return satellite.years();
} catch(WMSUninitializedException e) {
Logger.error(e.toString());
return null;
}
}
});
return async(
promise.map(
new Function<JsonNode, Result>() {
public Result apply(JsonNode node) {
if (node == null) {
return internalServerError();
} else {
return ok(node);
}
}
}));
}
public static Result list() {
DynamicForm form = Form.form().bindFromRequest();
final String year = form.get("year");
Promise<JsonNode> promise = Akka.future(
new Callable<JsonNode>() {
public JsonNode call() {
try {
if (year == null) {
return satellite.list();
} else {
return satellite.list(year);
}
} catch(WMSUninitializedException e) {
Logger.error(e.toString());
return null;
}
}
});
return async(
promise.map(
new Function<JsonNode, Result>() {
public Result apply(JsonNode node) {
if (node == null) {
return internalServerError();
} else {
return ok(node);
}
}
}));
}
public static Result map() {
DynamicForm form = Form.form().bindFromRequest();
final String layerName = form.get("layers");
final String format = form.get("format");
final String version = form.get("version");
final String width = form.get("width");
final String height = form.get("height");
final String srs = form.get("srs");
final String bbox = form.get("bbox");
Promise<InputStream> promise = Akka.future(
new Callable<InputStream>() {
public InputStream call() {
try {
return satellite.map(layerName, format, version, width, height, srs, bbox);
} catch(WMSUninitializedException e) {
Logger.error(e.toString());
return null;
}
}
});
return async(
promise.map(
new Function<InputStream, Result>() {
public Result apply(InputStream img) {
if (img == null) {
return internalServerError();
} else {
return ok(img).as(format);
}
}
}));
}
}
Для остальных спутников меняется только строка создания объекта Satellite - ему передаётся название соответствующего спутника.
Поскольку для получения данных по спутнику (списка изображений или самого изображения) может потребоваться подключение по сети к порталу Роскосмоса, это потенциально долгая операция. Чтобы избежать блокирования ("зависания") сервера, все эти операции оборачиваются в объекты Promise и обрабатываются асинхронно (async).
Клиентская часть
Клиентская часть работает в браузере и пишется на JavaScript. Для её структуризации используется паттерн MVC (Model-View-Controller). Модель (Model) описывает данные, представление (View) показывает их пользователю, а контроллер (Controller) соединяет одно с другим. Кроме того, мы ещё будем использовать стор (Store) для работы со списком моделей (объектов).
Модель и Стор
В нашем случае модель - это информация об отдельном снимке:
name - полный идентификатор снимка;
satellite - спутник, сделавший снимок;
date - дата снимка;
bbox - координаты снимка на карте
и ещё пара служебных полей:
GP.Module.Model.Roscosmosatellites = GP.Model.extend({
fields: [
{name: "name", type: "string", dvalue: '', notNull: true, rusName: "name"},
{name: "satelliteTitle", type: "string", dvalue: '', notNull: true, rusName: "satelliteTitle"},
{name: "satellite", type: "string", dvalue: "", notNull:true, rusName: "satellite"},
{name: "date", type: "string", dvalue: "", notNull:true, rusName: "date"},
{name: "srs", type: "string", dvalue: "", notNull:true, rusName: "srs"},
{name: "bbox", type: "object",dvalue: "", notNull:true,rusName:"bBox"}
]
});
Класс стора - просто стандартный стор для нашей модели:
GP.Module.Store.Roscosmosatellites = GP.Store.extend({
model: GP.Module.Model.Roscosmosatellites
});
Контроллер
Объект класса контроллер получает управление после загрузки страницы приложения в браузер. Поэтому на него ложится работа по загрузке дополнительных данных с сервера, начальному заполнению стора и инициализации виджета.
Наш контроллер выглядит так:
GP.Module.Controller.Roscosmosatellites = GP.Controller.extend({
statics: {
BASE_PATH: "/modules/roscosmosatellites/",
IMAGE_PATH: "/public/modules/roscosmosatellites/images/",
NUM_SATELLITES: 5
},
initialize: function() {
if ( GP.models.currentUser !== null ) {
this._logIn();
}
GP.controllers.application.on("reloadUser", function () {
if ( GP.models.currentUser !== null ) {
this._logIn();
} else {
this._logOut();
}
}, this);
},
_logIn: function () {
GP.Controller.prototype.initialize.call(this,{});
this._loadedCount = 0; // counter for a latch
this._data = []; // layers list cache
this._data[0] = {name: "meteor-m1"}; // Meteor-M1
this._data[1] = {name: "alos"}; // Alos
this._data[2] = {name: "kanopus-mss"}; // Kanopus-MSS
this._data[3] = {name: "kanopus-pss"}; // Kanopus-PSS
this._data[4] = {name: "resurs-dk1"}; // Resurs-DK1
/*
* Satellite object structure is as
* {
* name: "meteor-m1",
* years: ["2013", "2012", "2011"],
* "2013": [ <Layer>, <Layer>, ... ]
* }
*/
this._loadRoscosmosatellites();
GP.widgets.tabs.addTab({name : "Спутники", image : GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + "roscosmosatellites.png", imageWhite : GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + "roscosmosatellites-white.png", divId : "roscosmosatellites-block"});
},
_logOut: function () {
GP.widgets.tabs.removeTab("roscosmosatellites-block");
},
_loadSatelliteYears: function(satellite, index) {
$.ajax({
type: 'GET',
dataType: 'json',
url: GP.Module.Controller.Roscosmosatellites.BASE_PATH + satellite +'/years',
success: M.Util.bind(function(data) {
this._saveToCache({
data: data,
index: index
});
}, this),
error: function(data){
console.log("error ajax request");
}
});
},
_loadRoscosmosatellites: function(){
GP.fire("loader:show");
this._loadSatelliteYears("meteor-m1", 0);
this._loadSatelliteYears("alos", 1);
this._loadSatelliteYears("kanopus-mss", 2);
this._loadSatelliteYears("kanopus-pss", 3);
this._loadSatelliteYears("resurs-dk1", 4);
},
_saveToCache: function(dict){
var data = dict.data,
index = dict.index;
this._data[index].years = data;
this._loadedCount++;
// count-down latch for pure javascripter
if (this._loadedCount === GP.Module.Controller.Roscosmosatellites.NUM_SATELLITES) {
GP.fire("loader:close");
//Create widget
GP.widgets.roscosmosatellitesList = new GP.Module.Widget.RoscosmosatellitesList({
mainDivId:"roscosmosatellites-block",
data: this._data
});
}
}
});
GP.controllers.roscosmosatellites = new GP.Module.Controller.Roscosmosatellites();
С метода initialize начинается выполнение нашего кода после загрузки. В этом методе мы проверяем, вошёл ли пользователь в систему со своим логином и паролем. Если нет, то мы не показываем/убираем вкладку с нашим виджетом из правой панели (метод _logOut). Иначе - производим инициализацию модуля в методе _logIn.
В методе инициализации (_logIn) мы делаем три вещи:
создаём массив для кеширования данных о доступных изображениях (this._data = [];)
загружаем начальные данные со спутников (this._loadRoscosmosatellites();)
добавляем вкладку на правую панель (GP.widgets.tabs.addTab).
Кеш данных о доступных изображениях имеет сложную структуру - это массив, хранящий пять объектов, по одному на каждый спутник:
this._data[0] = {name: "meteor-m1"}; // Meteor-M1
this._data[1] = {name: "alos"}; // Alos
this._data[2] = {name: "kanopus-mss"}; // Kanopus-MSS
this._data[3] = {name: "kanopus-pss"}; // Kanopus-PSS
this._data[4] = {name: "resurs-dk1"}; // Resurs-DK1
Каждый объект, хранящий данные со спутника, строится по следующему шаблону:
{
name: "meteor-m1",
years: ["2013", "2012", "2011"],
"2013": [ <layer>, <layer>, ... ]
...
}
Т.е. для каждого спутника мы запоминаем годы, за которые доступны данные, и для каждого доступного года - метаданные обо всех снимках за этот год. Однако, данные о снимках загружаются не сразу, а только тогда когда они потребуются (этим занимается уже код виджета).
Метод _loadSatelliteYears отправляет на сервер запрос о доступных годах для заданного спутника, а полученные данные передаёт в метод _saveToCache вместе с индексом соответствующего спутнику объекта в массиве _data.
Метод _saveToCache делает две вещи. Прежде всего, он сохраняет полученный список доступных годов в кеш. Но поскольку метод вызывается несколько раз в неизвестные моменты времени и в неизвестной последовательности, которая определяется получением данных с сервера, нам необходимо определять момент, когда уже получены все данные. Поскольку мы заранее знаем, что метод будет вызван в общей сложности пять раз (поскольку мы работаем с пятью спутниками), используется счётчик завершённых вызовов (_loadedCount). Когда он достигает NUM_SATELLITES (5), мы создаём виджет для отображения списка снимков, передавая в него закешированные данные о спутниках:
GP.widgets.roscosmosatellitesList = new GP.Module.Widget.RoscosmosatellitesList({
mainDivId:"roscosmosatellites-block",
data: this._data
});
Техническое примечание. Для того чтобы указатель на текущий объект this сохранял своё значение, передавая методы в функции, которые будут их вызывать, мы оборачиваем методы в M.Util.bind(<method>, this). Этот же приём используется в коде виджета.
Виджет
Виджет реализует следующую функциональность:
позволяет выбрать спутник, год и месяц съёмки
показывает список снимков с выбранного спутника за указанный год и месяц
помещает выбранный снимок на карту
позволяет "перелететь" к изображению на карте
позволяет просмотреть подробную информацию о снимке
Поскольку виджет производит довольно много действий, код оказался весьма объёмным. Тем не менее, большинство методов виджета производят несложную вспомогательную работу. Ниже мы приводим код виджета целиком, после чего поясним основные методы.
GP.Module.Widget.RoscosmosatellitesList = GP.Widget.extend({
_abbrevations: {
"Метеор-М1": "ММ1",
"Алос": "Алос",
"Канопус-МСС": "МСС",
"Канопус-ПСС": "ПСС",
"Ресурс-ДК1": "ДК1"
},
dialogBoxStore: new Object,
_createWidget: function () {
if (!this.options.mainDivId || $("#" + this.options.mainDivId).length == 0)
throw("Can't create RoscosmosatellitesList widget. mainDivId parameter is undefined.");
this._mainElement = $("#" + this.options.mainDivId);
this._initElements();
this._satellitesSelect = null;
this._yearSelect = null;
this._monthSelect = null;
this._data = this.options.data;
this._currentSatellite = 0; // Meteor-M1 is a default
this._currentYear = this._data[this._currentSatellite].years[0];
this._currentMonth = "13"; // Stub, will be fixed by sanity-checking code
this._allYears = this._data[this._currentSatellite].years;
this._ensureDataAndSetupStore(M.Util.bind(function() {
// continuation
this._draw();
this._listElement.mCustomScrollbar({
advanced:{
updateOnBrowserResize:true,
updateOnContentResize:true
},
mouseWheel: true
}
);
this._resize();
GP.widgets.tabs.on("tab:resize",this._resize,this);
$('li[title="Спутники"]').on('click', M.Util.bind(function() {
this._resize();
}, this));
}, this));
},
_initElements: function() {
this._mainElement.append('<div class="block"><div class="listHead"></div><div class="layersList"></div></div>');
this._headElement = this._mainElement.find(".listHead");
this._filterBox = this._headElement.append('<div class="listHead--filters"></div>').find('.listHead--filters');
this._listElement = this._mainElement.find(".layersList");
this._roscosmosatellitesElement = this._mainElement.find(".roscosmosatellites");
this._table = this._listElement.append('<table class="satellites"><tbody></tbody></table>').find("tbody");
},
_clear: function() {
this._mainElement.empty();
},
_draw: function () {
GP.fire("loader:show");
this._drawSatellitesFilter();
this._yearSelect = this._filterBox.append("<select class='year_filter chzn-select'></select>").find(".year_filter");
this._monthSelect = this._filterBox.append("<select class='month_filter chzn-select'></select>").find(".month_filter");
this._drawInfoHead();
// fill year select
this._allYears.forEach(function(year) {
if (year === this._currentYear) {
this._yearSelect.append('<option selected="selected">' + year + '</option>');
} else {
this._yearSelect.append('<option>' + year + '</option>');
}
}, this);
// fill month select
this._monthsForCurrentYear().forEach(function(month) {
if (month === this._currentMonth) {
this._monthSelect.append('<option selected="selected">' + month + '</option>');
} else {
this._monthSelect.append('<option>' + month + '</option>');
}
}, this);
// draw selected layers if exists
if (this._addedLayers != null) {
this._addedLayers.forEach(function(element, index) {
this._table.append(element.dom);
this._table.children("tr:last").data( element.meta );
// console.log( element.meta );
}, this);
}
this._addedLayers = null;
//Use request animate for draw rows
var storeSize = GP.stores.roscosmosatellitesStore.length();
window.requestAnimFrame( M.Util.bind(function () {
this.drawEachRow( 0, storeSize, M.Util.bind(function() {
var checkbox = this._table.find("td.checkbox_cell input");
var target = this._table.find("td.target");
var info = this._table.find("td.info");
var widget = this;
this._bind(checkbox, "click", {self:this}, this._drawLayer);
this._bind(target, "click", {self:this}, this._goToLatLng);
this._bind(info, "click", {self:this}, function(){
var self = this;
widget._showDialogBox(self);
});
$(".chzn-select").chosen({disable_search_threshold: 12}).change(M.Util.bind(this._reloadList, this));
GP.fire("loader:close");
}, this));
}, this));
},
drawEachRow: function ( index, storeSize, continuation ) {
var cycleSize = index + 5;
if ( cycleSize < storeSize ) {
while ( index < cycleSize ) {
var data = GP.stores.roscosmosatellitesStore._dataArr[index];
this.createRow(data, index);
index++;
}
window.requestAnimFrame( M.Util.bind(function () {this.drawEachRow( index, storeSize, continuation )}, this) );
}
else if ( cycleSize >= storeSize ) {
while ( index < storeSize ) {
var data = GP.stores.roscosmosatellitesStore._dataArr[index];
this.createRow(data, index);
index++;
}
continuation();
}
},
createRow: function ( data, rowNumber ) {
var satelliteName = data.get('satellite'),
satelliteTitleStore = data.get('satelliteTitle'),
layerID = data.get('name').split("_")[2] ? data.get('name').split("_")[2] : data.get('name').split("_")[1],
geom = data.get('bbox'),
dateOrig = data.get('date'),
dateParsed = dateOrig.split("-"),
year = dateParsed[0],
month = dateParsed[1],
day = dateParsed[2],
date = day + '.' + month + '.' + year,
satelliteNameShort = this._abbrevations[satelliteTitleStore],
satelliteTitle = satelliteNameShort + "-" + layerID;
//Insert row stub into tbody
this._table.append('<tr class="row"></tr>');
//Prepear data for insertion into row
var row = this._table.children("tr:last").data({'satelliteTitle': satelliteTitleStore, 'date': date, 'layerName': data.get('name'), 'layerID': layerID, 'satelliteName': satelliteName, 'bbox': geom });
var rowData = '<td class="checkbox_cell">' +
'<input type="checkbox" class="checkbox">' +
'</td>' +
'<td class="satellite">' +
satelliteTitle +
'</td>' +
'<td class="date">' +
date +
'</td>' +
'<td class="target">' +
'<img src="' + GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + 'target.png' + '">' +
'</td>' +
'<td class="info">' +
'<img src="' + GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + 'info.png' + '">' +
'</td>';
// Add actual data to row
row.append(rowData);
if (rowNumber % 2 != 0)
row.addClass('colored');
row.hover(
function(){
$(this).addClass("onHover");
},
function(){
$(this).removeClass("onHover");
}
);
},
_drawSatellitesFilter: function() {
var html = '<select class="satellite_filter chzn-select">' +
'<option value="0">Метеор-М1</option>' +
'<option value="1">Алос</option>' +
'<option value="2">Канопус-МСС</option>' +
'<option value="3">Канопус-ПСС</option>' +
'<option value="4">Ресурс-ДК1</option>' +
'</select>';
this._satellitesSelect = this._filterBox.append(html).find(".satellite_filter");
this._satellitesSelect.find('option[value="' + this._currentSatellite + '"]').prop('selected', 'selected');
},
_drawInfoHead: function() {
var infoHeadData = '<table class="infoHead">' +
'<tbody>' +
'<tr>' +
'<td class="checkbox_cell"> </td>' +
'<td class="satellite" title="Спутник"><img src="' + GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + 'satellite_head.png' + '"></td>' +
'<td title="Дата съемки"><img src="' + GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + 'callendar_head.png' + '"></td>' +
'<td title="Перейти к области отображения снимка" class="target"><img src="' + GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + 'target_head.png' + '"></td>' +
'<td title="Подробная информация о слое" class="info"><img src="' + GP.Module.Controller.Roscosmosatellites.IMAGE_PATH + 'info_strong.png' + '"></td>' +
'</tr>' +
'<tbody>' +
'</table>';
this._headElement.append(infoHeadData);
},
_redraw: function() {
this._clear();
this._initElements();
this._draw();
this._listElement.mCustomScrollbar({
advanced:{
updateOnBrowserResize:true,
updateOnContentResize:true
},
mouseWheel: true
}
);
this._resize();
},
_resize: function(){
var cHeight = this._mainElement.height() - this._headElement.height();
this._listElement.css("height",cHeight);
setTimeout(M.Util.bind(function(){
this._listElement.mCustomScrollbar("update");
},this), 300);
},
_drawLayer: function(){
var layerElem = $(this).parents("tr"),
layerObj = layerElem.data('layerObj');
if(layerObj != undefined) {
GP.mainMap.map.removeLayer(layerObj);
layerObj = undefined;
layerElem.removeData('layerObj');
} else {
var layerName = layerElem.data("layerName"),
satelliteName = layerElem.data("satelliteName"),
url = "/modules/roscosmosatellites/" + satelliteName + '/map';
layerObj = new M.TileLayer.WMS(url, {layers: layerName, transparent: true, format: "image/png"});
GP.mainMap.map.addLayer(layerObj);
layerElem.data({'layerObj': layerObj});
}
},
_goToLatLng: function () {
var layerElem = $(this).parent("tr"),
layerName = layerElem.data("layerID"),
bboxObj = layerElem.data("bbox");
var bounds = new M.LatLngBounds(new M.LatLng(bboxObj['miny'],bboxObj['minx']), new M.LatLng(bboxObj['maxy'],bboxObj['maxx']));
GP.mainMap.map.fitBounds(bounds);
},
_showDialogBox: function (self) {
var layerElem = $(self).parents("tr"),
satelliteTitle = layerElem.data("satelliteTitle"),
layerName = layerElem.data("layerName"),
layerID = layerElem.data('layerID'),
date = layerElem.data("date");
var html = '<div class="satellineDialogBox"><table>' +
'<tbody>' +
'<tr>' +
'<td>Название слоя:</td>' +
'<td><strong>' + layerName + '</strong></td>' +
'</tr>' +
'<tr>' +
'<td>ID слоя:</td>' +
'<td><strong>' + layerID + '</strong></td>' +
'</tr>' +
'<tr>' +
'<td>Название спутника:</td>' +
'<td><strong>' + satelliteTitle + '</strong></td>' +
'</tr>' +
'<tr>' +
'<td>Дата съемки:</td>' +
'<td><strong>' + date + '</strong></td>' +
'</tr>' +
'</tbody>' +
'</table><hr></div>';
if ( !this.dialogBox ) {
this.dialogBox = new GP.Widget.DialogBox({dialogBoxId: "satelliteInfoBox",width: 380,top: 200},"#wrap");
}
this.dialogBox.setTitle(satelliteTitle + " — подробная информация.");
this.dialogBox.setContainer(html);
this.dialogBox.show();
},
_reloadList: function() {
var satelliteIndex = parseInt(this._satellitesSelect.val(), 10);
this._currentSatellite = satelliteIndex;
this._currentYear = this._yearSelect.val();
this._currentMonth = this._monthSelect.val();
// save checked layers
this._addedLayers = [];
this._table.children('tr:has(td.checkbox_cell input:checked)').each(M.Util.bind(function(index, elem) {
var layerElem = $(elem);
this._addedLayers[index] = {};
this._addedLayers[index].dom = layerElem;
this._addedLayers[index].meta = layerElem.data();
}, this));
this._allYears = this._data[this._currentSatellite].years;
if (this._allYears.indexOf(this._currentYear) < 0) {
this._currentYear = this._allYears[0];
}
this._ensureDataAndSetupStore(M.Util.bind(function() {
this._redraw();
}, this));
},
_ensureDataAndSetupStore: function(continuation) {
if (this._data[this._currentSatellite][this._currentYear] != undefined) {
this._setupStore(this._data[this._currentSatellite][this._currentYear]);
continuation();
} else {
this._loadLayers(this._data[this._currentSatellite].name, continuation);
}
},
_setupStore: function(layersList) {
// check sanity and fix parameters
var months = this._monthsForCurrentYear();
if (months.indexOf(this._currentMonth) < 0) {
this._currentMonth = months[0];
}
var data = [],
i = 0,
SIZE = layersList.length,
pattern = this._currentYear + "-" + this._currentMonth;
// seek for records for this._currentYear and this._currentMonth
while( (i < SIZE) && (layersList[i].date.indexOf(pattern) < 0) ) {
++i;
}
// save records for this._currentYear and this._currentMonth
while( (i < SIZE) && (layersList[i].date.indexOf(pattern) == 0) ) {
data.push(layersList[i]);
++i;
}
GP.stores.roscosmosatellitesStore = new GP.Module.Store.Roscosmosatellites(data);
},
_loadLayers: function(satellite, continuation) {
$.ajax({
type: 'GET',
dataType: 'json',
url: GP.Module.Controller.Roscosmosatellites.BASE_PATH + satellite +'/list?year=' + this._currentYear,
success: M.Util.bind(function(data) {
this._saveLayers(data, continuation);
}, this),
error: function(data){
console.log("error ajax request");
}
});
},
_saveLayers: function(data, continuation) {
this._data[this._currentSatellite][this._currentYear] = data;
this._setupStore(this._data[this._currentSatellite][this._currentYear]);
continuation();
},
_setLastYearAndMonth: function(layersList) {
var date = layersList[0].date,
dateParsed = date.split("-"),
year = dateParsed[0],
month = dateParsed[1];
this._currentYear = year;
this._currentMonth = month;
},
_monthsForCurrentYear: function() {
var months = [],
found = {};
this._data[this._currentSatellite][this._currentYear].forEach(function(layer) {
var date = layer.date,
dateParsed = date.split("-"),
month = dateParsed[1];
if (!found[month]) {
found[month] = true;
months.push(month);
};
}, this);
return months;
}
});
Метод _createWidget вызывается автоматически сразу же после создания объекта виджета. В нём мы привязываем виджет к соответствующему HTML-элементу на странице по его id (mainDivId), инициализируем поля объекта, которые будем позже использовать в методах и вызываем метод заполнения стора (_ensureDataAndSetupStore).
Методу _ensureDataAndSetupStore мы передаём параметр - функцию, которая должна быть вызвана после инициализации стора. В этой функции мы, в первую очередь, отрисовываем элементы управления и актуальный список снимков (метод _draw()) и инициируем пересчёт размеров виджета (метод _resize()).
Для хранения списка снимков, которые необходимо показать пользователю (соответствующих выбранному спутнику, году и месяцу), мы используем стор. Поэтому стор перезаполняется каждый раз, когда пользователь выбирает другой спутник, год или месяц. Данные для заполнения стора берутся из кеша (_data), но в кеше может не быть данных за указанный год для выбранного спутника - тогда эти данные предварительно загружаются с сервера (метод _loadLayers).
Стор создаётся и инициализируется в методе _setupStore. Этот метод проверяет, что за указанный месяц существуют снимки (иначе выбирает последний месяц, за который есть снимки) и выбирает их из кеша за указанный год. Поскольку сервер сортирует данные о снимках в обратном хронологическом порядке, все данные за один и тот же месяц идут в массиве подряд, так что мы собираем их простым циклом.
После заполнения стора вызывается метод _draw, который создаёт все необходимые HTML-элементы для отображения содержимого виджета. Сначала он создаёт элементы выпадающих списков (select) для выбора спутника, года и месяца, а в конце отрисовывает список изображений с помощью метода drawEachRow. Но, поскольку изображений бывает довольно много, отрисовка может занять продолжительное время. Чтобы интерфейс не замирал и пользователь видел, что информация о снимках постепенно появляется, мы отрисовываем не всё сразу, а пачками по 5 штук, используя функцию requestAnimFrame, чтобы дать браузеру возможность показать обновления и отреагировать на действия пользователя.
Методу drawEachRow тоже передаётся функция, которую необходимо вызвать после отрисовки данных по всем изображениям. Эта функция устанавливает обработчики щелчка мыши по элементам, добавляющим снимок на карту (_drawLayer), осуществляющим "перелёт" к изображению на карте (_goToLatLng) и показывающим подробные сведения (_showDialogBox).
Фактическое добавление данных о снимке в список производится методом createRow. Он получает объект модели снимка (взятый из стора) и номер строки (используется для "подсвечивания" нечётных строк). Из объекта модели извлекаются необходимые данные, которые подставляются в HTML-шаблон. Полученная строка добавляется в html-таблицу, отображающую список доступных снимков.
Метод _drawLayer добавляет на карту выбранный снимок (или удаляет, если снимок уже был добавлен). Для манипуляций со снимком используется библиотечный класс M.TileLayer.WMS, который самостоятельно скачивает с сервера изображение (по указанному URL) и управляет им. Поэтому для добавления снимка на карту достаточно вызова библиотечного метода GP.mainMap.map.addLayer(layerObj). Сам же объект изображения layerObj привязывается к записи об этом снимке в виджете вызовом layerElem.data({'layerObj': layerObj}).
Метод _goToLatLng извлекает данные об обрамляющем прямоугольнике (bounding box) выбранного снимка и осуществляет "перелёт" карты к нему вызовом метода GP.mainMap.map.fitBounds(bounds).
Метод _showDialogBox аналогично методу createRow извлекает данные о снимке, заполняет ими HTML-шаблон и создаёт диалоговое окно (new GP.Widget.DialogBox({dialogBoxId: "satelliteInfoBox",width: 380,top: 200},"#wrap")). После чего отображает в окне информацию о снимке: this.dialogBox.setContainer(html).
Остальные методы выполняют вспомогательную работу.