From 33705dc42b7abdfe4689d356c9637555161b69fd Mon Sep 17 00:00:00 2001 From: Benjamin Renard <benjamin.renard@akka.eu> Date: Thu, 9 Jan 2020 09:52:48 +0100 Subject: [PATCH] Catalog visu rework --- js/app/models/CatalogNode.js | 28 ++++++++++++++-------------- js/app/views/CatalogUI.js | 4 ++-- js/app/views/CatalogVisuHistogram.js | 34 ++++++++++++++++++++++++++++++++++ js/app/views/CatalogVisuScatter.js | 266 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ js/app/views/VisuUI.js | 463 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 5 files changed, 467 insertions(+), 328 deletions(-) create mode 100644 js/app/views/CatalogVisuHistogram.js create mode 100644 js/app/views/CatalogVisuScatter.js diff --git a/js/app/models/CatalogNode.js b/js/app/models/CatalogNode.js index 7350272..574c72e 100644 --- a/js/app/models/CatalogNode.js +++ b/js/app/models/CatalogNode.js @@ -1,4 +1,4 @@ -/** +/** * Project : AMDA-NG * Name : CatalogNode.js * @class amdaModel.CatalogNode @@ -10,20 +10,20 @@ Ext.define('amdaModel.CatalogNode', { extend: 'amdaModel.TimeTableNode', - + statics: { nodeType: 'catalog', objectName: 'Catalog' }, constructor : function(config) - { + { this.callParent(arguments) this.set('moduleId',myDesktopApp.dynamicModules.catalog.id); this.set('objectDataModel',amdaModel.Catalog.$className); if (this.get('leaf')) this.set('iconCls', 'icon-catalog'); }, - + localMenuItems : function() { var menuItems = [ @@ -45,7 +45,7 @@ Ext.define('amdaModel.CatalogNode', { hidden : true } ]; - + return menuItems; }, @@ -58,24 +58,24 @@ Ext.define('amdaModel.CatalogNode', { fnId : 'mult-downloadMulti', text : 'Download selected '+this.self.objectName+'s' }]; - + return menuItems; }, - shareNode: function(node) { + shareNode: function(node) { myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.catalog.id, true, function (module) { module.shareCatalog({'name' : node.get('text'), 'id' : node.get('id')}); }); }, - + visu : function(contextNode) { var me = this; - myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.visu.id, true, function (module) { + myDesktopApp.getLoadedModule(myDesktopApp.dynamicModules.visu.id, true, function (module) { // Catalog & TimeTable nodes normally use no objects in the tree var obj = {'id' : me.get('id'), 'name' : me.get('text') }; - object = Ext.create(me.get('objectDataModel'), obj); - me.set('object',object); - module.setLinkedNode(me); + object = Ext.create(me.get('objectDataModel'), obj); + me.set('object',object); + module.setLinkedNode(me); module.createWindow(); }); }, @@ -96,7 +96,7 @@ Ext.define('amdaModel.CatalogNode', { // edit newNode into Parameter Module with node as contextNode timeTabNode.editInModule(); }); - - + + } }); diff --git a/js/app/views/CatalogUI.js b/js/app/views/CatalogUI.js index d74c4f9..4d5c53c 100644 --- a/js/app/views/CatalogUI.js +++ b/js/app/views/CatalogUI.js @@ -93,7 +93,7 @@ Ext.define('amdaUI.CatalogUI', { if (updateStatus) { /// real object update // update TimeTable object with the content of form - basicForm.updateRecord(this.object); + basicForm.updateRecord(this.object.get('name')); } // return the update status return updateStatus; @@ -161,7 +161,7 @@ Ext.define('amdaUI.CatalogUI', { timeTabNode.editInModule(); }); }, - // Convert UTC date to client local date + // Convert UTC date to client local date convertUTCDateToLocalDate: function (date) { if (date == null) { return date; diff --git a/js/app/views/CatalogVisuHistogram.js b/js/app/views/CatalogVisuHistogram.js new file mode 100644 index 0000000..de1e397 --- /dev/null +++ b/js/app/views/CatalogVisuHistogram.js @@ -0,0 +1,34 @@ +/** + * Project AMDA-NG + * Name CatalogVisuHistogram.js + * @class amdaUI.CatalogVisuHistogram + * @extends Ext.container.Container + * @brief Histogram Visualization Module UI definition (View) + * @author elena + */ + +Ext.define('amdaUI.CatalogVisuHistogram', { + extend: 'Ext.form.Panel', + alias: 'widget.panelCatalogVisuHistogram', + + constructor: function(config) { + this.init(config); + this.callParent(arguments); + }, + + initVisu: function() { + + }, + + init : function (config) + { + var myConf = { + //layout: 'border', + items: [ + + ] + }; + + Ext.apply (this, Ext.apply(arguments, myConf)); + } +}); diff --git a/js/app/views/CatalogVisuScatter.js b/js/app/views/CatalogVisuScatter.js new file mode 100644 index 0000000..e796c03 --- /dev/null +++ b/js/app/views/CatalogVisuScatter.js @@ -0,0 +1,266 @@ +/** + * Project AMDA-NG + * Name CatalogVisuScatter.js + * @class amdaUI.CatalogVisuScatter + * @extends Ext.container.Container + * @brief Scatter Visualization Module UI definition (View) + * @author elena + */ + +Ext.define('amdaUI.CatalogVisuScatter', { + extend: 'Ext.form.Panel', + alias: 'widget.panelCatalogVisuScatter', + + constructor: function(config) { + this.init(config); + this.callParent(arguments); + }, + + getChart: function(chart) { + var store = Ext.create('Ext.data.Store', { + fields : [], + autoload : false + }); + + var xAxisTitleField = Ext.getCmp('visu-scatter-X-title'); + xAxisTitle = xAxisTitleField.getValue(); + if (!xAxisTitle || (xAxisTitle == '')) + xAxisTitle = 'X axis'; + + this.getAxisOptions('X'); + + var chartConfig = { + animate: false, + mask: false, + shadow: false, + theme:'Blue', + background: { fill : "#fff" }, + store: store, + axes: [{ + type: 'Numeric', + position: 'bottom', + fields: [], + title: xAxisTitle, + grid : true + }, { + type: 'Numeric', + position: 'left', + fields: [], + title: 'Y axis', + grid: true + }], + series: [{ + type: 'scatter', + showMarkers: true, + highlight: true, +// markerConfig: { + // radius: 5, +// size: 5 +// }, + // // axes: ['left', 'bottom'], + xField: '', + yField: '', +// label: { +// // display: 'under', +// // renderer: function(value, label, storeItem, item, i, display, animate, index) { +// // return storeItem.param3; +// // } +// }, + tips: { +// trackMouse: true, + width: 10, + height: 20, + hideDelay: 100, //200 ms + mouseOffset: [0,0], //[15,18] + renderer: function(storeItem, item) { + this.setTitle(storeItem.index + 1); + } + } + }] + }; + + return Ext.create('Ext.chart.Chart', chartConfig); + }, + + initVisu: function() { + var me = this; + }, + + getAxisOptions: function(axisName) { + var axisParamField = Ext.getCmp('visu-scatter-' + axisName + '-param'); + console.log(axisParamField.getValue()); + + + var axisTitleField = Ext.getCmp('visu-scatter-'+axisName+'-title'); + axisTitle = axisTitleField.getValue(); + if (!axisTitle || (axisTitle == '')) + axisTitle = axisName+' axis'; + }, + + getAxisConfig: function(axisIndex, axisName, parametersStore) { + var paramComboConfig = { + xtype: 'combo', + emptyText: 'select parameter', + editable: false, + store: parametersStore, + queryMode: 'local', + displayField: 'text', + valueField: 'id', + axisIndex: axisIndex, + id: 'visu-scatter-' + axisName + '-param', + tpl: Ext.create('Ext.XTemplate', + '<tpl for=".">', + '<div class="x-boundlist-item">{name}<tpl if="size > 1">[{index}]</tpl></div>', + '</tpl>' + ), + displayTpl: Ext.create('Ext.XTemplate', + '<tpl for=".">', + '{name}<tpl if="size > 1">[{index}]</tpl>', + '</tpl>' + ), + listeners : { + scope : this, + change : function(combo, newValue, oldValue) { + if (!combo.axisIndex || combo.axisIndex < 0) { + return; + } + if (newValue) { + /*this.chartConfig.axes[combo.axisIndex].fields = [newValue]; + var rec = combo.findRecordByValue(newValue); + + this.chartConfig.axes[combo.axisIndex].title = rec.get('text'); + this.chartConfig.series[combo.axisIndex].xField = newValue;*/ + console.log(combo.axisIndex); + } + } + } + }; + + var comboRangeConfig = { + xtype : 'combo', + name:'scaling', + valueField: 'scaling', + queryMode:'local', + store:['auto','manual'], + forceSelection:true, + value: 'auto', + width: 80, + listeners : { + scope : this, + change : function(combo, newValue, oldValue) { + var minValue = combo.next().next(); + var maxValue = minValue.next().next(); + var disabled = newValue == "auto"; + minValue.reset(); + maxValue.reset(); + minValue.setDisabled(disabled); + maxValue.setDisabled(disabled); + } + } + }; + + var axisRangeConfig = { + xtype : 'fieldcontainer', + layout: 'hbox', + items: [ + { + xtype:'fieldset', + title: axisName + ' Range', + border: false, + layout: 'hbox', + items: [ + comboRangeConfig, + { + xtype: 'splitter' + }, + { + xtype: 'numberfield', + hideTrigger: true, + width: 50, + disabled: true + }, + { + xtype: 'splitter' + }, + { + xtype: 'numberfield', + hideTrigger: true, + width: 50, + disabled: true + } + ] + } + ] + }; + + return { + xtype : 'fieldset', + title : axisName + ' axis', + items : [ + paramComboConfig, + axisRangeConfig, + { + xtype: 'textfield', + fieldLabel: axisName + ' title', + id: 'visu-scatter-' + axisName + '-title' + } + ] + }; + }, + + getPlottingOptionConfig: function() { + var plotTypeComboConfig = { + xtype: 'combo', + emptyText: 'select plot type', + editable: false, + store: ['scatter', 'line'], + queryMode: 'local', + valueField: 'type', + value: 'scatter', + listeners : { + scope : this, + change : function(combo, newValue, oldValue) { + //this.chartConfig.series[0].type = newValue; + } + } + }; + + var plotThemeComboConfig = { + xtype: 'combo', + emptyText: 'select theme', + editable: false, + store: ['Base','Green','Sky','Red','Purple','Blue','Yellow'], + queryMode: 'local', + valueField: 'type', + value: 'Blue', + listeners : { + scope : this, + change : function(combo, newValue, oldValue) { + //this.chartConfig.theme = newValue; + } + } + }; + + return { + xtype : 'fieldset', + title : 'Plotting Options', + items : [ + plotTypeComboConfig, + plotThemeComboConfig + ] + }; + }, + + init : function (config) + { + var myConf = { + items: [ + this.getAxisConfig(0,'X', config.parametersStore), + this.getAxisConfig(0,'Y', config.parametersStore), + this.getPlottingOptionConfig() + ] + }; + + Ext.apply (this, Ext.apply(arguments, myConf)); + } +}); diff --git a/js/app/views/VisuUI.js b/js/app/views/VisuUI.js index f7a89a0..260c9ac 100644 --- a/js/app/views/VisuUI.js +++ b/js/app/views/VisuUI.js @@ -11,6 +11,15 @@ Ext.define('amdaUI.VisuUI', { extend: 'Ext.container.Container', alias: 'widget.panelVisu', + requires: [ + 'amdaUI.CatalogVisuScatter', + 'amdaUI.CatalogVisuHistogram' + ], + + visuTabContents: [], + + parametersStore: null, + constructor: function(config) { this.init(config); this.callParent(arguments); @@ -67,7 +76,7 @@ Ext.define('amdaUI.VisuUI', { var onAfterInit = function(result, e) { - if (!result) { + /*if (!result) { myDesktopApp.errorMsg(e.message); Ext.defer(function(){Ext.Msg.toFront()},10); @@ -162,16 +171,28 @@ Ext.define('amdaUI.VisuUI', { // me.object.set('nbIntervals',me.chartStore.getTotalCount()); // load object into form - var formPanel = me.items.items[0].items.items[0]; - formPanel.getForm().loadRecord(me.object); + //BRE - var formPanel = me.items.items[0].items.items[0]; + //formPanel.getForm().loadRecord(me.object); } } }); - me.chartStore.load(); + me.chartStore.load();*/ } - AmdaAction.initForChart(this.object.get('id'), this.object.get('folderId'), this.object.get('objName'), this.fromPlugin, 'catalog', onAfterInit); + //AmdaAction.initForChart(this.object.get('id'), this.object.get('folderId'), this.object.get('objName'), this.fromPlugin, 'catalog', onAfterInit); + this.formPanel.getForm().loadRecord(this.object); + + this.parametersStore.removeAll(); + Ext.Array.each(this.object.get('parameters'), function(param) { + for (index = 0; index < param.size; ++index) { + me.parametersStore.add({'paramId': param.id+'_'+index, 'name' : param.name, 'index': index, 'size': param.size}); + } + }); + + Ext.Array.each(this.visuTabContents, function(content) { + content.initVisu(); + }); }, @@ -214,7 +235,7 @@ Ext.define('amdaUI.VisuUI', { plotChart : function () { - this.chartConfig.store = this.chartStore; + /*this.chartConfig.store = this.chartStore; var xTitle = this.items.items[0].items.items[0].items.items[1].items.items[2].getValue(); var yTitle = this.items.items[0].items.items[0].items.items[2].items.items[2].getValue(); @@ -248,7 +269,13 @@ Ext.define('amdaUI.VisuUI', { var chart = Ext.create('Ext.chart.Chart', this.chartConfig); - this.replaceChart(chart); + this.replaceChart(chart);*/ + var chart = Ext.getCmp('visu-chart'); + var tabPanel = Ext.getCmp('visu-tabpanel'); + + var chartPanel = chart.up(); + chartPanel.remove(chart); + chartPanel.insert(tabPanel.activeTab.items.items[0].getChart()); }, replaceChart: function(chart) { @@ -259,323 +286,133 @@ Ext.define('amdaUI.VisuUI', { chartPanel.insert(oldIndex, chart); }, + initChartTypes: function() { + var me = this; + + var tabPanel = Ext.getCmp('visu-tabpanel'); + if (!tabPanel) + return; + + var chartTypes = [ + { + title: 'Scatter', + widget: 'widget.panelCatalogVisuScatter' + }, + { + title: 'Histogram', + widget: 'widget.panelCatalogVisuHistogram' + } + ]; + + var isFirst = true; + Ext.Array.each(chartTypes, function(chartType) { + var tabContent = Ext.create(chartType.widget, {parametersStore : me.parametersStore}); + var tab = tabPanel.add({ + title: chartType.title, + items: [ + tabContent + ], + layout: 'fit' + }); + me.visuTabContents.push(tabContent); + if (isFirst) { + tabPanel.setActiveTab(tab); + isFirst = false; + } + }); + }, + init : function (config) { - var store = Ext.create('Ext.data.Store', { - fields : [], - autoload : false + this.parametersStore = Ext.create('Ext.data.Store', { + fields: [ + {name: 'paramId', type: 'string'}, + {name: 'name', type: 'string'}, + {name: 'index', type: 'int'}, + {name: 'size', type: 'int'} + ], + data: [] }); - var rangeStore = Ext.create('Ext.data.Store', { + var store = Ext.create('Ext.data.Store', { fields : [], autoload : false }); - this.chartConfig = { - width: 500, - height: 500, - animate: false, - mask: false, - shadow: false, - theme:'Blue', - background: { fill : "#fff" }, - store: store, - axes: [{ - type: 'Numeric', - position: 'bottom', - fields: [], - title: 'X axis', - grid : true - }, { - type: 'Numeric', - position: 'left', - fields: [], - title: 'Y axis', - grid: true - }], - series: [{ - type: 'scatter', - showMarkers: true, - highlight: true, -// markerConfig: { - // radius: 5, -// size: 5 -// }, - // // axes: ['left', 'bottom'], - xField: '', - yField: '', -// label: { -// // display: 'under', -// // renderer: function(value, label, storeItem, item, i, display, animate, index) { -// // return storeItem.param3; -// // } -// }, - tips: { -// trackMouse: true, - width: 10, - height: 20, - hideDelay: 100, //200 ms - mouseOffset: [0,0], //[15,18] - renderer: function(storeItem, item) { - this.setTitle(storeItem.index + 1); - } - } - }] - } - - this.parList = Ext.create('Ext.data.Store', { - fields : [ 'text', 'id'] - }); - - var chart = Ext.create('Ext.chart.Chart', this.chartConfig); - - this.parCombo = Ext.create('Ext.form.ComboBox', { - emptyText: 'select parameter', - editable: false, - store: this.parList, - queryMode: 'local', - displayField: 'text', - valueField: 'id', - // tpl:'<tpl for="."><div ext:qtip="{qtip}" class="x-combo-list-item">{Name}</div></tpl>', - listeners : { - scope : this, - change : function(combo, newValue, oldValue) { - if (newValue) { - this.chartConfig.axes[0].fields = [newValue]; - var rec = combo.findRecordByValue(newValue); - - this.chartConfig.axes[0].title = rec.get('text'); - this.chartConfig.series[0].xField = newValue; - } - } - } - }); - - this.parCombo1 = Ext.create('Ext.form.ComboBox', { - emptyText: 'select parameter', - editable: false, - store: this.parList, - queryMode: 'local', - displayField: 'text', - valueField: 'id', - listeners : { - scope : this, - change : function(combo, newValue, oldValue) { - if (newValue) { - this.chartConfig.axes[1].fields = [newValue]; - var rec = combo.findRecordByValue(newValue); - this.chartConfig.axes[1].title = rec.get('text'); - this.chartConfig.series[0].yField = newValue; - } - } - } - }); - - var plotTypeCombo = Ext.create('Ext.form.ComboBox', { - emptyText: 'select plot type', - editable: false, - store: ['scatter', 'line'], - queryMode: 'local', - valueField: 'type', - value: 'scatter', - listeners : { - scope : this, - change : function(combo, newValue, oldValue) { - this.chartConfig.series[0].type = newValue; - } - } - }); - - var plotThemeCombo = Ext.create('Ext.form.ComboBox', { - emptyText: 'select theme', - editable: false, - store: ['Base','Green','Sky','Red','Purple','Blue','Yellow'], - //'Category1','Category2','Category3','Category4','Category5','Category6'], - queryMode: 'local', - valueField: 'type', - value: 'Blue', - listeners : { - scope : this, - change : function(combo, newValue, oldValue) { - this.chartConfig.theme = newValue; - } - } - }); - - var comboRangeConfig = { - // fieldLabel:'X Range', - name:'scaling', - valueField: 'scaling', - queryMode:'local', - store:['auto','manual'], - forceSelection:true, - value: 'auto', - width: 80, - listeners : { - scope : this, - change : function(combo, newValue, oldValue) { - var minValue = combo.next().next(); - var maxValue = minValue.next().next(); - var disabled = newValue == "auto"; - minValue.reset(); - maxValue.reset(); - minValue.setDisabled(disabled); - maxValue.setDisabled(disabled); - } - } - }; - - this.comboXrange = Ext.create('Ext.form.ComboBox', comboRangeConfig); - this.comboYrange = Ext.create('Ext.form.ComboBox', comboRangeConfig); - - var formPanel = Ext.create('Ext.form.Panel', { - region : 'center', - layout: 'hbox', + this.formPanel = Ext.create('Ext.form.Panel', { + region: 'center', + layout: 'border', bodyStyle: {background : '#dfe8f6'}, defaults: { border : false, align: 'stretch', padding: '3'}, - fieldDefaults: { labelWidth: 80, labelAlign : 'top' }, - items: [ { - xtype: 'form', - flex : 1, - bodyStyle: {background : '#dfe8f6'}, - items: [{ - xtype : 'fieldset', - items : [{ - xtype: 'fieldcontainer', - layout: 'hbox', - items: [ - { xtype:'textfield', fieldLabel: 'Catalog Name', name: 'name', readOnly: true}, - { xtype: 'splitter' }, - { xtype:'textfield', fieldLabel: 'Intervals', name: 'nbIntervals', readOnly: true} - ] - }] - },{ - xtype : 'fieldset', - title : 'X axis', - items : [ - this.parCombo, - { - xtype : 'fieldcontainer', - layout: 'hbox', - items: [{ - xtype:'fieldset', - title: 'X Range', - border: false, - layout: 'hbox', - items: [ - this.comboXrange, - { - xtype: 'splitter' - }, { - xtype: 'numberfield', - hideTrigger: true, - width: 50, - disabled: true - },{ - xtype: 'splitter' - },{ - xtype: 'numberfield', - hideTrigger: true, - width: 50, - disabled: true - }] - }] - }, -// { xtype : 'checkbox', boxLabel: 'Logarithmic'}, - { xtype: 'textfield', fieldLabel: 'X title', name: 'xtitle'} - ] - },{ - xtype : 'fieldset', - title : 'Y axis', - items : [ - this.parCombo1, - { - xtype : 'fieldcontainer', - layout: 'hbox', - items: [{ - xtype:'fieldset', - title: 'Y Range', - border: false, - layout: 'hbox', - items: [ - this.comboYrange, - { - xtype: 'splitter' - }, { - xtype: 'numberfield', - hideTrigger: true, - width: 50, - disabled: true - },{ - xtype: 'splitter' - },{ - xtype: 'numberfield', - hideTrigger: true, - width: 50, - disabled: true - }] - }] - }, -// { xtype : 'checkbox', boxLabel: 'Logarithmic', -// listeners: { -// scope: this, -// change : function( check, newValue, oldValue) { -// -// } -// } -// }, - { xtype: 'textfield', fieldLabel: 'Y title', name: 'ytitle'} - ] - }, - { - xtype : 'fieldset', - title : 'Plotting Options', - items : [ - plotTypeCombo, - plotThemeCombo - ] - } - ], - fbar:[{ - type: 'button', - text: 'Plot', - scope : this, - handler: this.plotChart - - },{ - type: 'button', - text: 'Reset', - scope : this, - handler: this.reset - - }] - }, { - xtype: 'form', - // padding: '3', - flex: 2, - items : [ chart ], - fbar:[ - { - type: 'button', - text: 'Save Chart', - scope: this, - handler: function() { - var chartPanel = this.items.items[0].items.items[1]; - var chart = chartPanel.down('chart'); - chart.save({ - type: 'image/png', - defaultUrl : window.location //'http://apus.irap.omp.eu/NEWAMDA/' - }); - } - }] - } - ] - }); - - var myConf = { + items: [ + { + xtype : 'fieldset', + region: 'north', + items : [ + { + xtype: 'fieldcontainer', + layout: 'hbox', + fieldDefaults: { labelWidth: 80, labelAlign : 'right' }, + items: [ + { xtype:'textfield', fieldLabel: 'Catalog Name', name: 'name', readOnly: true}, + { xtype: 'splitter' }, + { xtype:'textfield', fieldLabel: 'Intervals', name: 'nbIntervals', readOnly: true} + ] + } + ], + }, + { + xtype: 'container', + region: 'center', + layout: 'border', + items: [ + { + xtype: 'tabpanel', + region: 'west', + width: 250, +// height: 400, + id: 'visu-tabpanel' + }, + { + xtype: 'chart', + region: 'center', + store: store, + id: 'visu-chart', + animate: false, + mask: false, + shadow: false, + theme:'Blue', + background: { fill : "#fff" } + } + ] + } + ], + fbar:[ + { + type: 'button', + text: 'Plot', + scope : this, + handler: this.plotChart + }, + { + type: 'button', + text: 'Reset', + scope : this, + handler: this.reset + }, + { + type: 'button', + text: 'Save Chart', + scope : this, + handler: this.saveChart + } + ] + }); + + var myConf = { layout: 'border', items: [ - formPanel, + this.formPanel, { xtype: 'panel', region: 'south', @@ -594,6 +431,8 @@ Ext.define('amdaUI.VisuUI', { ] }; + this.initChartTypes(); + Ext.apply (this, Ext.apply(arguments, myConf)); } }); -- libgit2 0.21.2