oxjs/source/Ox.UI/js/Map/Ox.ListMap.js
2011-11-05 17:46:53 +01:00

905 lines
28 KiB
JavaScript

// vim: et:ts=4:sw=4:sts=4:ft=javascript
'use strict';
/*@
Ox.ListMap <f:Ox.Element> ListMap object
() -> <f> ListMap object
(options) -> <f> ListMap object
(options, self) -> <f> ListMap object
options <o> Options object
height <n|256> Height in px
labels <b|false> If true, show labels
places <a|f|null> Array of places, or function that returns places
selected <a|[]> Selected places
width <n|256> Width in px
self <o> Shared private variable
@*/
Ox.ListMap = function(options, self) {
self = self || {};
var that = Ox.Element({}, self)
.defaults({
addPlace: null,
editPlace: null,
getMatches: null,
height: 256,
labels: false,
pageLength: 100,
places: null,
removePlace: null,
selected: [],
showControls: false,
showLabels: false,
showTypes: false,
sort: [{key: 'geoname', operator: '+'}],
width: 256
})
.options(options || {})
.addClass('OxListMap')
.css({
width: self.options.width + 'px',
height: self.options.height + 'px'
});
self.isAsync = Ox.isFunction(self.options.places);
self.columns = [
{
addable: false, // fixme: implement
id: 'id',
title: 'Id',
unique: true,
visible: false,
width: 64
},
{
format: function(value) {
return $('<img>')
.attr({
// fixme: not the right place to do this
src: Ox.PATH + 'Ox.Geo/png/icons/16/' + (value || 'NTHH') + '.png'
})
.css({
width: '14px',
height: '14px',
borderRadius: '4px',
marginLeft: '-3px',
marginTop: 0
});
},
id: 'countryCode',
resizable: false, // fixme: implement
title: 'Flag',
titleImage: 'flag',
visible: true,
width: 16
},
{
id: 'name',
operator: '+',
removable: false,
title: 'Name',
visible: true,
width: 144
},
{
format: function(value) {
return value.join('; ');
},
id: 'alternativeNames',
operator: '+',
title: 'Alternative Names',
visible: true,
width: 144
},
{
id: 'geoname',
map: function(value) {
var names = value.split(', ');
if (!Ox.getCountryByGeoname(names[names.length - 1])) {
names.push('~');
}
return names.reverse().join(', ');
},
operator: '+',
title: 'Geoname',
visible: true,
width: 192
},
{
format: function(value) {
return Ox.toTitleCase(value);
},
id: 'type',
operator: '+',
title: 'Type',
visible: true,
width: 64
},
{
align: 'right',
format: toFixed,
id: 'south',
operator: '+',
title: 'South',
visible: false,
width: 96
},
{
align: 'right',
id: 'west',
operator: '+',
title: 'West',
visible: false,
width: 96
},
{
align: 'right',
format: toFixed,
id: 'north',
operator: '+',
title: 'North',
visible: false,
width: 96
},
{
align: 'right',
format: toFixed,
id: 'east',
operator: '+',
title: 'East',
visible: false,
width: 96
},
{
align: 'right',
format: toFixed,
id: 'lat',
operator: '+',
title: 'Latitude',
visible: true,
width: 96
},
{
align: 'right',
format: toFixed,
id: 'lng',
operator: '+',
title: 'Longitude',
visible: true,
width: 96
},
{
align: 'right',
format: {type: 'area', args: []},
id: 'area',
operator: '-',
title: 'Area',
visible: true,
width: 128
},
{
id: 'user',
operator: '+',
title: 'User',
visible: false,
width: 96
},
{
format: function(value) {
return value.replace('T', ' ').replace('Z', '');
},
id: 'created',
operator: '-',
title: 'Date Created',
visible: false,
width: 128,
},
{
format: function(value) {
return value.replace('T', ' ').replace('Z', '');
},
id: 'modified',
operator: '-',
title: 'Date Modified',
visible: false,
width: 128,
},
{
align: 'right',
id: 'matches',
operator: '-',
title: 'Matches',
visible: false,
width: 64,
}
];
self.$listToolbar = Ox.Bar({
size: 24
});
self.$findElement = Ox.FormElementGroup({
elements: [
self.$findSelect = Ox.Select({
items: [
{id: 'all', title: 'Find: All'},
{id: 'name', title: 'Find: Name'},
{id: 'alternativeNames', title: 'Find: Alternative Names'},
{id: 'geoname', title: 'Find: Geoname'}
],
overlap: 'right',
type: 'image'
})
.bindEvent({
change: function(data) {
var key = data.selected[0].id,
value = self.$findInput.value();
value && updateList(key, value);
}
}),
self.$findInput = Ox.Input({
clear: true,
placeholder: 'Find in List',
width: 234
})
.bindEvent({
submit: function(data) {
var key = self.$findSelect.value(),
value = data.value;
updateList(key, value);
}
})
]
})
.css({float: 'right', margin: '4px'})
.bindEvent({
change: function(data) {
}
})
.appendTo(self.$listToolbar);
self.$list = Ox.TextList({
columns: self.columns,
columnsRemovable: true,
columnsVisible: true,
//items: Ox.clone(self.options.places),
items: self.options.places,
pageLength: self.options.pageLength,
scrollbarVisible: true,
sort: self.options.sort
})
.bindEvent({
'delete': removeItem,
init: initList,
// fixme: do we need 0/shift-0? return already zooms to place
key_0: function() {
self.$map.panToPlace();
},
key_equal: function() {
self.$map.zoom(1);
},
key_minus: function() {
self.$map.zoom(-1);
},
key_shift_0: function() {
self.$map.zoomToPlace();
},
load: function() {
that.triggerEvent('loadlist');
},
open: openItem,
select: selectItem
});
self.$listStatusbar = Ox.Bar({
size: 16
});
self.$status = Ox.Element()
.css({paddingTop: '2px', margin: 'auto', fontSize: '9px', textAlign: 'center'})
.appendTo(self.$listStatusbar);
self.$map = Ox.Map({
clickable: true,
editable: true,
findPlaceholder: 'Find on Map',
height: self.options.height,
places: self.options.places,
//statusbar: true,
showControls: self.options.showControls,
showLabels: self.options.showLabels,
showTypes: self.options.showTypes,
toolbar: true,
width: self.options.width - 514,//self.mapResize[1],
zoombar: true
})
.bindEvent({
/*
addplace: function(data) {
that.triggerEvent('addplace', data);
},
*/
changeplace: function(data) {
self.$placeForm.values(data).show();
self.$areaKmInput.options({value: Ox.formatArea(data.area)});
},
changeplaceend: function(data) {
//Ox.Log('Map', 'ssP', self.selectedPlace);
var isResult = self.selectedPlace[0] == '_';
!isResult && editPlace([
'lat', 'lng', 'south', 'west', 'north', 'east', 'area'
]);
},
geocode: function(data) {
that.triggerEvent('geocode', data);
},
/*
resize: function() {
self.$map.resizeMap(); // fixme: don't need event
},
*/
selectplace: selectPlace
});
self.$placeTitlebar = Ox.Bar({
size: 24
});
self.$placeTitle = $('<div>')
.hide()
.appendTo(self.$placeTitlebar)
self.$placeFlag = $('<img>')
.addClass('OxFlag')
.attr({
src: Ox.getImageByGeoname('icon', 16, '')
})
.css({float: 'left', margin: '4px'})
.appendTo(self.$placeTitle);
self.$placeName = Ox.Label({
title: '',
width: 208
})
.css({float: 'left', margin: '4px 0 4px 0'})
.appendTo(self.$placeTitle)
.bindEvent({
singleclick: function() {
self.$map.panToPlace();
},
doubleclick: function() {
self.$map.zoomToPlace();
}
});
self.$deselectPlaceButton = Ox.Button({
title: 'close',
tooltip: 'Done',
type: 'image'
})
.css({float: 'left', margin: '4px'})
.bindEvent({
click: function() {
self.$map.options({selected: null});
}
})
.appendTo(self.$placeTitle);
self.$nameInput = Ox.Input({
id: 'name',
label: 'Name',
labelWidth: 64,
width: 240
}).bindEvent({
change: function(data) {
var isResult = self.selectedPlace[0] == '_';
!isResult && self.$list.value(self.selectedPlace, 'name', data.value);
if (!self.isAsync) {
Ox.getObjectById(
self.options.places, self.selectedPlace
).name = data.value;
} else {
!isResult && editPlace(['name']);
}
self.$map.value(self.selectedPlace, 'name', data.value);
}
});
self.$alternativeNamesInput = Ox.ArrayInput({
id: 'alternativeNames',
label: 'Alternative Names',
max: 10,
//sort: true,
values: [],
width: 240
}).bindEvent({
change: function(data) {
var isResult = self.selectedPlace[0] == '_';
if (!self.isAsync) {
} else {
!isResult && editPlace(['alternativeNames'])
}
self.$map.value(self.selectedPlace, 'alternativeNames', data.value);
}
});
self.$geonameInput = Ox.Input({
id: 'geoname',
label: 'Geoname',
labelWidth: 64,
width: 240
}).bindEvent({
change: function(data) {
var geoname = data.value,
country = Ox.getCountryByGeoname(geoname),
countryCode = country ? country.code : '',
isResult = self.selectedPlace[0] == '_';
self.$placeFlag.attr({
src: Ox.getImageByGeoname('icon', 16, geoname)
});
self.$placeName.options({title: geoname});
self.$placeForm.values({countryCode: countryCode});
if (!self.isAsync) {
if (!isResult) {
self.$list.value(self.selectedPlace, 'geoname', geoname);
self.$list.value(self.selectedPlace, 'countryCode', countryCode);
}
} else {
!isResult && editPlace(['countryCode', 'geoname']);
}
self.$map.value(self.selectedPlace, 'countryCode', countryCode);
self.$map.value(self.selectedPlace, 'geoname', geoname);
}
});
// fixme: form should have a change event
// fixme: it has one now, but inputs fire on blur
self.$placeFormItems = Ox.merge([
self.$nameInput,
self.$alternativeNamesInput,
self.$geonameInput,
Ox.Input({
id: 'countryCode'
}).hide(),
Ox.Select({
id: 'type',
items: [
{id: 'country', title: 'Country'},
{id: 'region', title: 'Region'}, // administative (Kansas) or colloquial (Midwest)
{id: 'city', title: 'City'},
{id: 'borough', title: 'Borough'},
{id: 'street', title: 'Street'}, // streets, squares, bridges, tunnels, ...
{id: 'building', title: 'Building'}, // airports, stations, stadiums, military installations, ...
{id: 'feature', title: 'Feature'} // continents, islands, rivers, lakes, seas, oceans, ...
],
label: 'Type',
labelWidth: 64,
width: 240
}).bindEvent({
change: function(data) {
var isResult = self.selectedPlace[0] == '_';
if (!self.isAsync) {
} else {
!isResult && editPlace(['type'])
}
self.$map.value(self.selectedPlace, 'type', data.selected[0].id);
}
})
], ['Latitude', 'Longitude', 'South', 'West', 'North', 'East'].map(function(v) {
var id = (
v == 'Latitude' ? 'lat' : v == 'Longitude' ? 'lng' : v
).toLowerCase(),
max = ['Latitude', 'South', 'North'].indexOf(v) > -1 ? Ox.MAX_LATITUDE : 180;
return Ox.Input({
decimals: 8,
disabled: ['lat', 'lng'].indexOf(id) > -1,
id: id,
label: v,
labelWidth: 80,
min: -max,
max: max,
type: 'float',
width: 240
})
.bindEvent({
blur: function(data) {
///*
// fixme: if type is set, no change event fires
var isResult = self.selectedPlace[0] == '_';
if (!self.isAsync) {
} else {
!isResult && editPlace([v])
}
self.$map.value(self.selectedPlace, id, parseFloat(data.value));
//*/
}
});
}), [
self.$areaInput = Ox.Input({
id: 'area',
type: 'float'
}).hide()
]);
self.$placeForm = Ox.Form({
items: self.$placeFormItems,
width: 240
})
.css({margin: '8px'})
.hide();
self.$areaKmInput = Ox.Input({
disabled: true,
id: 'areaKm',
label: 'Area',
labelWidth: 80,
textAlign: 'right',
width: 240
})
.css({margin: '4px 0 4px 0'})
.appendTo(self.$placeForm);
if (self.options.getMatches) {
self.$matchesInput = Ox.Input({
disabled: true,
id: 'matches',
label: 'Matches',
labelWidth: 80,
type: 'int',
width: 240
})
.css({margin: '8px 0 4px 0'})
.appendTo(self.$placeForm);
}
self.$placeStatusbar = Ox.Bar({
size: 24
});
self.$newPlaceButton = Ox.Button({
title: 'New Place',
width: 96
})
.css({float: 'left', margin: '4px 2px 4px 4px'})
.bindEvent({
click: function() {
self.$map.newPlace();
}
})
.appendTo(self.$placeStatusbar);
self.$placeButton = Ox.Button({
title: 'Add Place',
width: 96
})
.css({float: 'right', margin: '4px 4px 4px 2px'})
.bindEvent({
click: function() {
if (self.$placeButton.options('title') == 'Add Place') {
addPlace();
} else {
removePlace();
}
}
})
.hide()
.appendTo(self.$placeStatusbar);
/*
self.$revertButton = Ox.Button({
title: 'Revert',
width: 96
})
.css({float: 'right', margin: '4px 4px 4px 2px'})
.hide()
.appendTo(self.$placeStatusbar);
*/
/*
self.mapResize = [
Math.round(self.options.width * 0.25),
Math.round(self.options.width * 0.5),
Math.round(self.options.width * 0.75)
];
*/
/*
if (!self.isAsync) {
self.placesLength = self.options.places.length;
setStatus();
} else {
self.options.places({}, function(results) {
self.placesLength = results.data.items;
setStatus();
});
}
*/
that.$element.replaceWith(
that.$element = Ox.SplitPanel({
elements: [
{
collapsible: true,
element: Ox.SplitPanel({
elements: [
{
element: self.$listToolbar,
size: 24
},
{
element: self.$list
},
{
element: self.$listStatusbar,
size: 16
}
],
orientation: 'vertical'
}),
resizable: true,
resize: [256, 384, 512],
size: 256
},
{
element: self.$map,
},
{
collapsible: true,
element: Ox.SplitPanel({
elements: [
{
element: self.$placeTitlebar,
size: 24
},
{
element: self.$placeForm
},
{
element: self.$placeStatusbar,
size: 24
}
],
orientation: 'vertical'
})
.bindEvent({
resize: function(data) {
self.$placeName.options({width: data.size - 48});
// fixme: pass width through form
self.$placeFormItems.forEach(function($item) {
$item.options({width: data.size - 16});
});
}
}),
resizable: true,
resize: [204, 256, 384],
size: 256
}
],
orientation: 'horizontal'
}).$element
);
function addPlace() {
var place = self.$placeForm.values(),
country = Ox.getCountryByGeoname(place.geoname);
place.countryCode = country ? country.code : '';
if (!self.isAsync) {
place.id = self.selectedPlace.substr(1); // fixme: safe?
self.selectedPlace = place.id;
self.options.places.push(place);
self.$list.options({
items: Ox.clone(self.options.places),
selected: [place.id]
});
self.$map.addPlace(place);
self.$placeButton.options({title: 'Remove Place'});
//setStatus();
}
//that.triggerEvent('addplace', {place: place});
if (self.isAsync) {
self.$placeButton.options({disabled: true, title: 'Adding Place'});
self.options.addPlace(place, function(result) {
if (result.status.code == 200) {
place.id = result.data.id;
self.selectedPlace = place.id;
self.$list.reloadList().options({selected: [place.id]});
self.$map.addPlace(place);
self.$placeButton.options({disabled: false, title: 'Remove Place'});
} else {
if (result.data.names) {
if (result.data.names.indexOf(self.$nameInput.value()) > -1) {
self.$nameInput.addClass('OxError');
}
self.$alternativeNamesInput.setErrors(result.data.names);
}
if (result.data.geoname) {
self.$geonameInput.addClass('OxError');
}
self.$placeButton.options({disabled: false, title: 'Add Place'});
}
});
}
}
function editPlace(keys) {
var values = Ox.filter(self.$placeForm.values(), function(values, key) {
return keys.indexOf(key) > -1;
});
values.id = self.selectedPlace;
self.options.editPlace(values, function() {
if (keys.indexOf(self.$list.options('sort')[0].key) > -1) {
self.$list.reloadList();
} else {
Ox.forEach(values, function(value, key) {
if (key != 'id') {
self.$list.value(values.id, key, value);
self.$map.value(values.id, key, value);
}
});
}
});
if (keys.indexOf('name') > -1 || keys.indexOf('alternativeNames') > -1) {
updateMatches();
}
}
function initList(data) {
self.$status.html(
Ox.formatNumber(data.items) + ' Place' + (
data.items == 1 ? '' : 's'
)
);
}
function openItem(data) {
selectItem(data);
self.$map.zoomToPlace(data.ids[0]);
}
function removeItem(data) {
var id = data.ids[0];
// fixme: events or callback functions??
that.triggerEvent('removeplace', {id: id});
self.$map.removePlace(id);
}
function removePlace() {
var index;
Ox.Log('Map', 'REMOVE PLACE', self.selectedPlace, index)
if (!self.isAsync) {
// fixme: doesn't call self.options.removePlace!
index = Ox.getPositionById(self.options.places, self.selectedPlace);
self.options.places.splice(index, 1);
self.$list.options({items: Ox.clone(self.options.places)});
//setStatus();
}
// fixme: what is this? both options.removePlace and event removeplace??
if (self.isAsync) {
self.options.removePlace({id: self.selectedPlace}, function() {
self.$list.options({selected: []}).reloadList(true);
});
}
self.$map.removePlace();
self.$placeButton.options({title: 'Add Place'});
that.triggerEvent('removeplace', {id: self.selectedPlace});
}
function selectItem(data) {
var id = data.ids.length ? data.ids[0] : null;
self.$map.options({selected: id});
id && self.$map.panToPlace();
}
function selectPlace(place) {
var isResult = place.id && place.id[0] == '_';
self.$list.options({
selected: place.id && !isResult ? [place.id] : []
});
if (place.id) {
//isResult && self.options.places.push(place);
self.selectedPlace = place.id;
self.$placeFlag.attr({
src: Ox.getImageByGeoname('icon', 16, place.geoname)
});
self.$placeName.options({title: place.geoname || ''});
self.$placeTitle.show();
self.$areaKmInput.options({value: Ox.formatArea(place.area)});
self.$placeForm.values(place).show();
self.$placeButton.options({title: isResult ? 'Add Place' : 'Remove Place'}).show();
updateMatches();
} else {
self.selectedPlace = null;
self.$placeTitle.hide();
self.$placeForm.hide();
self.$placeButton.hide();
}
}
function toFixed(val) {
return Ox.isNumber(val) ? val.toFixed(8) : val; // fixme: why can a string be passed ??
}
function updateList(key, value) {
var query = {
conditions: Ox.merge(
['all', 'name'].indexOf(key) > -1
? [{key: 'name', value: value, operator: '='}] : [],
['all', 'alternativeNames'].indexOf(key) > -1
? [{key: 'alternativeNames', value: value, operator: '='}] : [],
['all', 'geoname'].indexOf(key) > -1
? [{key: 'geoname', value: value, operator: '='}] : []
),
operator: key == 'all' ? '|' : '&'
};
self.$list.options({
items: function(data, callback) {
return pandora.api.findPlaces(Ox.extend(data, {
query: query
}), callback);
}
});
}
function updateMatches() {
var names, place;
if (self.options.getMatches) {
place = self.$placeForm.values();
names = Ox.filter(Ox.merge([place.name], place.alternativeNames), function(name) {
return name !== '';
});
if (names.length) {
self.$matchesInput.options({value: ''});
self.options.getMatches(names, function(matches) {
self.$matchesInput.options({value: Ox.formatNumber(matches)});
});
} else {
self.$matchesInput.options({value: 0});
}
}
}
/*@
setOption <f> setOption
@*/
self.setOption = function(key, value) {
if (key == 'height') {
self.$list.size();
self.$map.resizeMap();
} else if (key == 'selected') {
self.$list.options({selected: value});
} else if (key == 'width') {
self.$map.resizeMap();
}
}
/*@
focusList <f> focusList
@*/
that.focusList = function() {
self.$list.gainFocus();
return that;
}
/*@
reloadList <f> reloadList
@*/
that.reloadList = function() {
self.$list.reloadList();
return that;
}
/*@
resizeMap <f> resizeMap
@*/
that.resizeMap = function() {
Ox.Log('Map', 'Ox.ListMap.resizeMap()')
self.$map.resizeMap();
return that;
};
return that;
};