game/static/game/js/level_editor.js
'use strict';
var ocargo = ocargo || {};
ocargo.LevelEditor = function(levelId) {
/*************/
/* Constants */
/*************/
var LIGHT_RED_URL = ocargo.Drawing.raphaelImageDir + 'trafficLight_red.svg';
var LIGHT_GREEN_URL = ocargo.Drawing.raphaelImageDir + 'trafficLight_green.svg';
var COW_GROUP_COLOR_PALETTE = [
// From https://en.wikipedia.org/wiki/Help:Distinguishable_colors
'#FFFFFF', // White
'#FFA405', // Orpiment
'#0075DC', // Blue
'#C20088', // Mallow
'#9DCC00', // Lime
'#FF5005', // Zinnia
'#808080', // Iron
'#F0A3FF', // Amethyst
'#993F00', // Caramel
'#740AFF', // Violet
'#FFFF00', // Wine
'#4C005C', // Damson
'#94FFB5', // Jade
'#FFCC99', // Honeydew
'#00998F', // Turqoise
'#E0FF66', // Uranium
'#005C31', // Forest
'#FFFF80', // Xanthin
'#5EF1F2', // Sky
'#003380', // Navy
'#8F7C00' // Khaki
];
var ADD_ROAD_IMG_URL = ocargo.Drawing.imageDir + "icons/add_road.svg";
var DELETE_ROAD_IMG_URL = ocargo.Drawing.imageDir + "icons/delete_road.svg";
var MARK_START_IMG_URL = ocargo.Drawing.imageDir + "icons/origin.svg";
var MARK_END_IMG_URL = ocargo.Drawing.imageDir + "icons/destination.svg";
var VALID_LIGHT_COLOUR = '#87E34D';
var INVALID_LIGHT_COLOUR = '#E35F4D';
var paper = $('#paper'); // May as well cache this
var modes = {
ADD_ROAD_MODE: {
name: gettext('Add road'),
url: ocargo.Drawing.imageDir + 'icons/add_road.svg',
id: 'add_road',
},
DELETE_ROAD_MODE: {
name: gettext('Delete road'),
url: ocargo.Drawing.imageDir + 'icons/delete_road.svg',
id: 'delete_road',
},
MARK_DESTINATION_MODE: {
name: gettext('Mark end'),
url: ocargo.Drawing.imageDir + 'icons/destination.svg',
id: 'end',
},
MARK_ORIGIN_MODE: {
name: gettext('Mark start'),
url: ocargo.Drawing.imageDir + 'icons/origin.svg',
id: 'start',
},
};
/*********/
/* State */
/*********/
var saving = new ocargo.Saving();
var drawing = new ocargo.Drawing();
drawing.preloadRoadTiles();
// Level information
var nodes = [];
var decor = [];
var trafficLights = [];
var cows = [];
var originNode = null;
var destinationNode = null;
var currentTheme = THEMES.grass;
// Reference to the Raphael elements for each square
var grid;
// Current mode the user is in
var mode = modes.ADD_ROAD_MODE;
var prevMode = null;
// Holds the state for when the user is drawing or deleting roads
var strikeStart = null;
var saveState = new ocargo.LevelSaveState();
var ownedLevels = new ocargo.OwnedLevels(saveState);
var sharing = new ocargo.Sharing(
() => saveState.id,
() => canShare() && isLevelOwned()
);
// Whether the user is scrolling on a tablet
var isScrolling = false;
// Whether the trashcan is currently open
var trashcanOpen = false;
var trashcanAbsolutePaperX;
var trashcanAbsolutePaperY;
var cowGroups = {};
var currentCowGroupId = 1;
if (NIGHT_MODE_FEATURE_ENABLED) {
$('#play_night_tab').show()
}
// Setup max_fuel
setupMaxFuel();
// Initialise the grid
initialiseGrid();
setTheme(THEMES.grass);
// Setup the toolbox
setupToolbox();
if (levelId !== null) {
loadLevel(levelId);
}
setupTrashcan();
// Draw everything
drawAll();
/*********************************/
/* Two finger scrolling of paper */
/*********************************/
var scrollStartPosX = 0;
var scrollStartPosY = 0;
var touchStartPosX = 0;
var touchStartPosY = 0;
paper.on('touchstart', function(ev) {
if (ev.originalEvent.touches.length === 2) {
ev.preventDefault();
scrollStartPosX = paper.scrollLeft();
touchStartPosX = ev.originalEvent.touches[0].pageX;
scrollStartPosY = paper.scrollTop();
touchStartPosY = ev.originalEvent.touches[0].pageY;
isScrolling = true;
}
});
paper.on('touchmove', function(ev) {
if (ev.originalEvent.touches.length === 2) {
ev.preventDefault();
paper.scrollLeft(scrollStartPosX - (ev.originalEvent.touches[0].pageX - touchStartPosX));
paper.scrollTop(scrollStartPosY - (ev.originalEvent.touches[0].pageY - touchStartPosY));
}
});
paper.on('touchend touchcancel', function(ev) {
if (ev.originalEvent.touches.length === 0) {
isScrolling = false;
}
});
/***************/
/* Setup tools */
/***************/
// Sets up the left hand side toolbox (listeners/images etc.)
function setupToolbox() {
var tabs = [];
var currentTabSelected = null;
tabs.play = new ocargo.Tab($('#play_radio'), $("#run-code-button"), $('#play_radio + label'));
tabs.playNight = new ocargo.Tab($('#play_night_radio'), $('#play_night_radio + label'));
tabs.map = new ocargo.Tab($('#map_radio'), $('#map_radio + label'), $('#map_pane'));
tabs.scenery = new ocargo.Tab($('#scenery_radio'), $('#scenery_radio + label'), $('#scenery_pane'));
tabs.character = new ocargo.Tab($('#character_radio'), $('#character_radio + label'), $('#character_pane'));
tabs.blocks = new ocargo.Tab($('#blocks_radio'), $('#blocks_radio + label'), $('#blocks_pane'));
tabs.random = new ocargo.Tab($('#random_radio'), $('#random_radio + label'), $('#random_pane'));
tabs.load = new ocargo.Tab($('#load_radio'), $('#load_radio + label'), $('#load_pane'));
tabs.save = new ocargo.Tab($('#save_radio'), $('#save_radio + label'), $('#save_pane'));
tabs.share = new ocargo.Tab($('#share_radio'), $('#share_radio + label'), $('#share_pane'));
tabs.help = new ocargo.Tab($('#help_radio'), $('#help_radio + label'));
tabs.quit = new ocargo.Tab($('#quit_radio'), $('#quit_radio + label'));
setupPlayTab();
setupPlayNightTab();
setupMapTab();
setupSceneryTab();
setupCharacterTab();
setupBlocksTab();
setupRandomTab();
setupLoadTab();
setupSaveTab();
setupShareTab();
setupHelpTab();
setupQuitTab();
ownedLevels.update();
// enable the map tab by default
currentTabSelected = tabs.map;
tabs.map.select();
function restorePreviousTab() {
currentTabSelected.select();
}
function playFunction(night) {
function playLevel(url) {
var nightSuffix = night ? '?night=1' : '';
window.location.href = url + nightSuffix;
}
return function() {
if (isLevelValid()) {
var state = extractState();
state.name = "Custom level";
if (hasLevelChangedSinceSave()) {
saving.saveLevel(state, null, true, function (levelId) {
playLevel(Urls.play_anonymous_level(levelId));
}, console.error);
} else {
playLevel(Urls.play_custom_level_from_editor(saveState.id));
}
} else {
restorePreviousTab();
}
};
}
function setupPlayTab() {
tabs.play.setOnChange(playFunction(false));
}
function setupPlayNightTab() {
tabs.playNight.setOnChange(playFunction(true));
}
function changeCurrentToolDisplay(mode){
$('#currentToolText').text(mode.name);
$('#currentToolIcon').attr("src", mode.url);
Object.values(modes).forEach((element) => {
$(`#${element.id}`).addClass('unselected');
});
$(`#${mode.id}`).removeClass('unselected');
}
function setupMapTab() {
tabs.map.setOnChange(function() {
transitionTab(tabs.map);
mode = modes.ADD_ROAD_MODE;
changeCurrentToolDisplay(modes.ADD_ROAD_MODE);
});
$('#clear').click(function() {
clear();
drawAll();
});
$('#start').click(function() {
mode = modes.MARK_ORIGIN_MODE;
changeCurrentToolDisplay(modes.MARK_ORIGIN_MODE);
});
$('#end').click(function() {
mode = modes.MARK_DESTINATION_MODE;
changeCurrentToolDisplay(modes.MARK_DESTINATION_MODE);
});
$('#add_road').click(function() {
mode = modes.ADD_ROAD_MODE;
changeCurrentToolDisplay(modes.ADD_ROAD_MODE);
});
$('#delete_road').click(function() {
mode = modes.DELETE_ROAD_MODE;
changeCurrentToolDisplay(modes.DELETE_ROAD_MODE);
});
if(DEVELOPER) {
$('#djangoText').click(function() {
ocargo.Drawing.startPopup('Django level migration',
'Copy the text in the console into the Django migration file.',
'You will have to change the level name and fill in the model solution field.');
});
}
}
function setupSceneryTab() {
tabs.scenery.popup = true;
tabs.scenery.setOnChange(function() {
transitionTab(tabs.scenery);
});
$('#theme_select').change(function() {
var selectedValue = $(this).val();
var theme = THEMES[selectedValue];
if (theme) {
setTheme(theme);
}
});
$('.decor_button').click(function(e){
new InternalDecor(e.target.id);
});
$('#trafficLightRed').click(function() {
new InternalTrafficLight({"redDuration": 3, "greenDuration": 3, "startTime": 0,
"startingState": ocargo.TrafficLight.RED,
"sourceCoordinate": null, "direction": null});
});
$('#trafficLightGreen').click(function() {
new InternalTrafficLight({"redDuration": 3, "greenDuration": 3, "startTime": 0,
"startingState": ocargo.TrafficLight.GREEN,
"sourceCoordinate": null, "direction": null});
});
if(COW_LEVELS_ENABLED) {
if (Object.keys(cowGroups).length == 0) {
addCowGroup();
}
$('#cow').click(function () {
new InternalCow({group: cowGroups["group1"]});
});
}
}
function setupCharacterTab() {
tabs.character.setOnChange(function() {
transitionTab(tabs.character);
});
$("#character_select").change(function() {
var selectedValue = $(this).val();
var character = CHARACTERS[selectedValue];
if (character) {
var CHARACTER_NAME = character.name;
$('#character_image').attr('src', character.image);
redrawRoad();
}
});
$("#character_select").change();
}
function setupBlocksTab() {
tabs.blocks.setOnChange(function() {
transitionTab(tabs.blocks);
});
setupBlocks();
// Initial selection
$('#move_forwards_checkbox').prop('checked', true);
$('#turn_left_checkbox').prop('checked', true);
$('#turn_right_checkbox').prop('checked', true);
// Select all functionality
var selectAll = $('#select_all_checkbox');
selectAll.change(function() {
var checked = selectAll.prop('checked');
$('.block_checkbox').each(function() {
if ($(this) !== selectAll) {
$(this).prop('checked', checked);
}
});
});
// Language controls
$('#language_select').change(function() {
var value = $(this).val();
$('#blockly_blocks_div').css('display', this.value === 'python' ? 'none' : 'block');
});
function setupBlocks() {
function addListenerToImage(type) {
$('#' + type + '_image').click(function() {
$('#' + type + '_checkbox').click();
});
}
// Setup the block images
initCustomBlocksDescription();
// Hacky, if a way can be found without initialising the entire work space that would be great!
var blockly = document.getElementById('blockly');
var toolbox = document.getElementById('blockly_toolbox');
Blockly.inject(blockly, {
path: '/static/game/js/blockly/',
toolbox: toolbox,
trashcan: true
});
for (var i = 0; i < BLOCKS.length; i++) {
var type = BLOCKS[i];
var block = Blockly.mainWorkspace.newBlock(type);
block.initSvg();
block.render();
var svg = block.getSvgRoot();
var large = type === "controls_whileUntil" ||
type === "controls_repeat" ||
type === "controls_if" ||
type === "declare_proc" ||
type === "controls_repeat_while" ||
type === "controls_repeat_until";
var content = '<svg class="block_image' + (large ? ' large' : '') + '">';
content += '<g transform="translate(10,0)"';
content += svg.innerHTML ? svg.innerHTML : '';
content += '</g></svg>';
$('#' + type + '_image').html(content);
addListenerToImage(type);
}
// Hide blockly
$('#blockly').css('display','none');
}
}
function setupRandomTab() {
tabs.random.setOnChange(function() {
transitionTab(tabs.random);
});
$('#generate').click(function() {
var numberOfTiles = Math.max(Math.min($('#size').val(), 40), 2);
var branchiness = Math.max(Math.min($('#branchiness').val(), 10), 0);
var loopiness = Math.max(Math.min($('#loopiness').val(), 10), 0);
var curviness = Math.max(Math.min($('#curviness').val(), 10), 0);
$('#size').val(numberOfTiles);
$('#branchiness').val(branchiness);
$('#loopiness').val(loopiness);
$('#curviness').val(curviness);
var data = {numberOfTiles: numberOfTiles,
branchiness: branchiness/10.0,
loopiness: loopiness/10.0,
curviness: curviness/10.0,
trafficLights: $('#trafficLightsEnabled').val() == "yes",
scenery: $('#sceneryEnabled').val() == "yes"};
$('#generate').attr('disabled', true);
saving.retrieveRandomLevel(data, function(error, mapData) {
if (error) {
console.error(error);
ocargo.Drawing.startInternetDownPopup();
return;
}
restoreState(mapData);
$('#generate').attr('disabled', false);
});
});
}
function goToMapTab() {
tabs.map.select();
}
function setupLoadTab() {
var selectedLevel = null;
var listOfOwnedLevels = null;
var sharedLevels = [];
ownedLevels.addListener(processListOfOwnedLevels);
function updateSharedLevels() {
saving.retrieveSharedLevels(processListOfSharedLevels, processError);
}
tabs.load.setOnChange(function() {
if(!isLoggedIn("load")) {
restorePreviousTab();
return;
}
transitionTab(tabs.load);
updateSharedLevels();
});
// Setup own/shared levels radio
$('#load_type_select').change(function() {
var ownLevelsSelected = this.value === "ownLevels";
var sharedLevelsSelected = this.value === "sharedLevels";
var levels = ownLevelsSelected ? listOfOwnedLevels : sharedLevels;
populateLoadSaveTable("loadLevelTable", levels);
// Add click listeners to all rows
var rows = $('#loadLevelTable tr[value]');
rows.on('click', function(event) {
$('#loadLevelTable tr').attr('selected', false);
$('#loadLevelTable tr').css('selected', false);
$(this).attr('selected', true);
$('#loadLevel').removeAttr('disabled');
$('#deleteLevel').attr('disabled', sharedLevelsSelected);
selectedLevel = $(this).attr('value');
});
rows.on('dblclick', loadSelectedLevel);
$('#deleteLevel').attr('disabled', sharedLevelsSelected || !selectedLevel);
$('#loadLevel').attr('disabled', !selectedLevel);
$('#load_pane .scrolling-table-wrapper').css('display', levels.length === 0 ? 'none' : 'block');
});
function loadSelectedLevel() {
if (selectedLevel) {
loadLevel(selectedLevel);
goToMapTab();
}
}
$('#loadLevel').click(loadSelectedLevel);
$('#deleteLevel').click(function() {
if (!selectedLevel) {
return;
}
var levelId = selectedLevel;
ownedLevels.deleteLevel(levelId);
});
function processError(err) {
console.error(err);
restorePreviousTab();
ocargo.Drawing.startInternetDownPopup();
return;
}
function adjustPaneDisplay() {
if (listOfOwnedLevels.length == 0 && sharedLevels.length == 0) {
$('#load_pane #does_exist').css('display', 'none');
$('#load_pane #does_not_exist').css('display', 'block');
} else {
$('#load_pane #does_exist').css('display', 'block');
$('#load_pane #does_not_exist').css('display', 'none');
}
}
function reloadList() {
$('#load_type_select').change();
}
function processListOfOwnedLevels(newOwnedLevels) {
listOfOwnedLevels = newOwnedLevels;
// Important: done before change() call
// Table cells need to have rendered to match th with td widths
reloadList();
// But disable all the modal buttons as nothing is selected yet
selectedLevel = null;
adjustPaneDisplay();
}
function processListOfSharedLevels(listOfSharedLevels) {
sharedLevels = listOfSharedLevels;
reloadList();
// But disable all the modal buttons as nothing is selected yet
selectedLevel = null;
adjustPaneDisplay();
}
}
function setupSaveTab() {
var selectedLevel = null;
ownedLevels.addListener(processListOfLevels);
tabs.save.setOnChange(function () {
//getLevelTextForDjangoMigration();
if (!isLoggedIn("save") || !isLevelValid()) {
restorePreviousTab();
return;
}
transitionTab(tabs.save);
});
function save() {
if(!isLevelValid()) {
return;
}
var newName = $('#levelNameInput').val();
if (!newName || newName === "") {
// TODO error message?
return;
}
var regex = /^(\w?[ ]?)*$/;
var validString = regex.exec($('#levelNameInput').val());
if (!validString) {
ocargo.Drawing.startPopup(gettext('Oh no!'), gettext('You used some invalid characters.'),
gettext('Try saving your level again using only letters and numbers.'));
return;
}
function saveLevelLocal(existingId) {
saveLevel(newName, existingId, goToMapTab);
}
// Test to see if we already have the level saved
var table = $("#saveLevelTable");
var existingId = -1;
for (var i = 0; i < table[0].rows.length; i++) {
var row = table[0].rows[i];
var existingName = row.cells[0].innerHTML;
if (existingName === newName) {
existingId = row.getAttribute('value');
break;
}
}
if (existingId != -1) {
if (!saveState.isCurrentLevel(existingId)) {
var onYes = function(){
saveLevelLocal(existingId);
$("#myModal").hide()
$("#ocargo-modal").hide()
};
var onNo = function(){
$("#myModal").hide()
$("#ocargo-modal").hide()
};
ocargo.Drawing.startYesNoPopup(gettext('Overwriting'), gettext('Warning'),
interpolate(gettext('Level %(level_name)s already exists. Are you sure you want to overwrite it?'), {
level_name: newName
}, true), onYes, onNo);
} else {
saveLevelLocal(existingId);
}
} else {
saveLevelLocal(null);
}
}
$('#saveLevel').click(save);
function processListOfLevels(ownedLevels) {
// Important: done before change() call
// Table cells need to have rendered to match th with td widths
populateLoadSaveTable("saveLevelTable", ownedLevels);
// Add click listeners to all rows
var rows = $('#saveLevelTable tr[value]');
rows.on('click', function(event) {
var rowSelected = $(event.target.parentElement);
$('#saveLevelTable tr').attr('selected', false);
$(this).attr('selected', true);
$('#saveLevel').removeAttr('disabled');
selectedLevel = parseInt(rowSelected.attr('value'));
for (var i = 0; i < ownedLevels.length; i++) {
if (ownedLevels[i].id === selectedLevel) {
$("#levelNameInput").val(ownedLevels[i].name);
}
}
});
rows.on('dblclick', save);
$('#save_pane .scrolling-table-wrapper').css('display', ownedLevels.length === 0 ? 'none' : 'block');
selectedLevel = null;
}
}
function setupShareTab() {
// Setup the behaviour for when the tab is selected
tabs.share.setOnChange(function() {
if (!isIndependentStudent() || !isLoggedIn("share") || !canShare() || !isLevelOwned()) {
restorePreviousTab();
return;
}
saving.getSharingInformation(saveState.id, function(error, validRecipients) {
if(error) {
console.error(error);
return;
}
transitionTab(tabs.share);
sharing.processSharingInformation(error, validRecipients);
});
});
sharing.setupTeacherPanel();
sharing.setupSelectAllButton();
}
function setupHelpTab() {
var helpMessages = [
gettext('To get started, draw a road.'),
gettext('Click on the square you want the road to start from. Then, without letting go of the mouse button, ' +
'drag to the square you’d like the road to end on. Do this as many times as you like to add new sections ' +
'of road.'),
interpolate(
gettext('In %(map_icon)s%(map_label)s menu, click %(mark_start_icon)s%(mark_start_label)s and select a ' +
'square for your road to start from. The starting point can only be placed on dead ends. You need a ' +
'road first before adding a starting point. Make sure you use %(mark_end_icon)s%(mark_end_label)s to ' +
'select a final destination. Setting a fuel level means the route will need to be short enough for the ' +
'fuel not to run out.'
), {
map_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/map.svg', 'popupIcon'),
map_label: '<b>' + gettext('Map') + '</b>',
mark_start_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/origin.svg', 'popupIcon'),
mark_start_label: '<b>' + gettext('Mark start') + '</b>',
mark_end_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/destination.svg', 'popupIcon'),
mark_end_label: '<b>' + gettext('Mark end') + '</b>'
},
true
),
interpolate(
gettext('To remove road, click the %(delete_road_icon)s%(delete_road_label)s button and select a section ' +
'to get rid of.'
), {
delete_road_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/delete_road.svg', 'popupIcon'),
delete_road_label: '<b>' + gettext('Delete road') + '</b>'
},
true
),
interpolate(
gettext('Click %(random_icon)s%(random_label)s if you want the computer to create a random route for you.'), {
random_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/random.svg', 'popupIcon'),
random_label: '<b>' + gettext('Random') + '</b>'
},
true
),
interpolate(
gettext('Select %(scenery_icon)s%(scenery_label)s and choose trees, bushes and more to place around your ' +
'road. These will show in the top left corner - drag them into place. Delete items by dragging them ' +
'into the bin in the bottom right. To rotate a traffic light, simply double click on it. Remember, ' +
'using the traffic lights is not covered until level 44.'
), {
scenery_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/decor.svg', 'popupIcon'),
scenery_label: '<b>' + gettext('Scenery') + '</b>'
},
true
),
interpolate(
gettext('Choose a character to play with from the %(character_icon)s%(character_label)s menu.'), {
character_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/character.svg', 'popupIcon'),
character_label: '<b>' + gettext('Character') + '</b>'
},
true
),
interpolate(
gettext('Select which blocks of code you want to be used to create a route for your character from the ' +
'%(blocks_icon)s%(blocks_label)s menu.'
), {
blocks_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/blockly.svg', 'popupIcon'),
blocks_label: '<b>' + gettext('Blocks') + '</b>'
},
true
),
interpolate(
gettext('When you\'re ready click %(play_icon)s%(play_label)s, or %(save_icon)s%(save_label)s your road or ' +
'%(share_icon)s%(share_label)s it with a friend. Don\'t forget to choose a good name for it!'
), {
play_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/play.svg', 'popupIcon'),
play_label: '<b>' + gettext('Play') + '</b>',
save_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/save.svg', 'popupIcon'),
save_label: '<b>' + gettext('Save') + '</b>',
share_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/share.svg', 'popupIcon'),
share_label: '<b>' + gettext('Share') + '</b>'
},
true
),
interpolate(
gettext('%(quit_icon)s%(quit_label)s will take you back to the Rapid Router homepage.'
), {
quit_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/quit.svg', 'popupIcon'),
quit_label: '<b>' + gettext('Quit') + '</b>'
},
true
)
];
tabs.help.setOnChange(function() {
restorePreviousTab();
ocargo.Drawing.startPopup('', '', helpMessages.join('<br><br>'));
});
}
function setupQuitTab() {
tabs.quit.setOnChange(function() {
window.location.href = Urls.levels();
});
}
// Helper methods
function transitionTab(newTab) {
var previousTab = currentTabSelected;
previousTab.setPaneEnabled(false);
newTab.setPaneEnabled(true);
currentTabSelected = newTab;
return previousTab;
}
function populateLoadSaveTable(tableName, levels) {
var table = $('#' + tableName + ' tbody');
$('#' + tableName).css('display', levels.length == 0 ? 'none' : 'table');
$('#' + tableName + 'Header').css('display', levels.length == 0 ? 'none' : 'table');
// Remove click listeners to avoid memory leak and remove all rows
$('#' + tableName + ' tr').off('click');
table.empty();
// Order them alphabetically
levels.sort(function (a, b) {
if (a.name < b.name) {
return -1;
} else if (a.name > b.name) {
return 1;
}
return 0;
});
// Add a row to the table for each level saved in the database
for (var i = 0, ii = levels.length; i < ii; i++) {
var level = levels[i];
var row = $('<tr></tr>').attr({ value: level.id }).appendTo(table);
$('<td></td>').text(level.name).appendTo(row);
$('<td></td>').text(level.owner).appendTo(row);
}
for (var i = 0; i < 2; i++) {
var td = $('#' + tableName + ' td:eq(' + i + ')');
var td2 = $('#' + tableName + 'Header th:eq(' + i + ')');
td2.width(td.width());
}
}
}
/************/
/* Cows */
/************/
function addCowGroup() {
if(COW_LEVELS_ENABLED) {
var color = COW_GROUP_COLOR_PALETTE[(currentCowGroupId - 1) % COW_GROUP_COLOR_PALETTE.length];
var style = 'background-color: ' + color;
var value = 'group' + currentCowGroupId++;
var type = ocargo.Cow.WHITE;
cowGroups[value] = {
id: value,
color: color,
minCows: 1,
maxCows: 1,
type: type
};
var text = interpolate(gettext('Group %(cow_group)s'), {cow_group: Object.keys(cowGroups).length}, true);
$('#cow_group_select').append($('<option>', {value: value, style: style})
.text(text));
$('#cow_group_select').val(value).change();
}
}
function removeCowGroup() {
if(Object.keys(cowGroups).length > 1) {
var selectedGroupId = $('#cow_group_select').val();
//Remove cows from map
for(var i = cows.length - 1; i >= 0; i--) {
if(cows[i].data.group.id === selectedGroupId) {
cows[i].destroy();
}
}
//Remove group from group list
delete cowGroups[selectedGroupId];
// Select previous option select element if present
var selectedOption = $('#cow_group_select > option:selected');
if(selectedOption.prev('option').length > 0) {
selectedOption.prev('option').attr('selected', 'selected');
} else {
selectedOption.next('option').attr('selected', 'selected');
}
// Remove old option
selectedOption.remove();
// Trigger change event on select element
$('#cow_group_select').change();
//Renumber groups in select element
var groupNo = 1;
$('#cow_group_select').find('option').each(function() {
$(this).text(interpolate(gettext('Group %(cow_group)s'), {cow_group: groupNo++}, true));
});
}
}
/************/
/* MaxFuel */
/************/
function setupMaxFuel(){
var MAX_FUEL = 99;
var DEFAULT_FUEL = 50;
var lastCorrectFuel = $('#max_fuel').val();
if (!onlyContainsDigits(lastCorrectFuel)) {
$('#max_fuel').val(DEFAULT_FUEL);
lastCorrectFuel = DEFAULT_FUEL;
}
$('#max_fuel').on('input', function () {
var value = $(this).val();
$(this).val(updatedValue(value));
});
function restrictValue(value){
if (value > MAX_FUEL) {
return MAX_FUEL;
} else {
return value;
}
}
function updatedValue(value){
if (onlyContainsDigits(value)) {
value = parseInt(value);
var newValue = restrictValue(value);
lastCorrectFuel = newValue;
return newValue;
} else {
return lastCorrectFuel;
}
}
function onlyContainsDigits(n){
return n !== '' && /^\d+$/.test(n);
}
}
/************/
/* Trashcan */
/************/
function placeTrashcan(trashcan) {
// Iffy way of making sure the trashcan stays inside the grid
// when window bigger than grid
var windowWidth = $(window).width();
var windowHeight = $(window).height();
var paperRightEdge = PAPER_WIDTH + $('#tools').width();
var paperBottomEdge = PAPER_HEIGHT;
var bottom = 50;
if(windowHeight > paperBottomEdge) {
bottom += windowHeight - paperBottomEdge
}
var right = 50;
if(windowWidth > paperRightEdge) {
right += windowWidth - paperRightEdge;
}
trashcan.css('right', right);
trashcan.css('bottom', bottom);
var trashcanOffset = trashcan.offset();
var paperOffset = paper.offset();
trashcanAbsolutePaperX = trashcanOffset.left - paperOffset.left;
trashcanAbsolutePaperY = trashcanOffset.top - paperOffset.top;
}
function setupTrashcan() {
var trashcan = $('#trashcanHolder');
placeTrashcan(trashcan);
$(window).resize(function() {
placeTrashcan(trashcan);
});
addReleaseListeners(trashcan[0]);
closeTrashcan();
}
function checkImageOverTrashcan(paperX, paperY, imageWidth, imageHeight) {
var paperAbsX = paperX - paper.scrollLeft() + imageWidth/2;
var paperAbsY = paperY - paper.scrollTop() + imageHeight/2;
var trashcanWidth = $('#trashcanHolder').width();
var trashcanHeight = $('#trashcanHolder').height();
if(paperAbsX > trashcanAbsolutePaperX && paperAbsX <= trashcanAbsolutePaperX + trashcanWidth &&
paperAbsY > trashcanAbsolutePaperY - 20 && paperAbsY <= trashcanAbsolutePaperY + trashcanHeight) {
openTrashcan();
} else {
closeTrashcan();
}
}
function openTrashcan() {
$('#trashcanLidOpen').css('display', 'block');
$('#trashcanLidClosed').css('display', 'none');
trashcanOpen = true;
}
function closeTrashcan() {
$('#trashcanLidOpen').css('display', 'none');
$('#trashcanLidClosed').css('display', 'block');
trashcanOpen = false;
}
/************************/
/** Current state tests */
/************************/
// Functions simply to improve readability of complex conditional code
function isOriginCoordinate(coordinate) {
return originNode && originNode.coordinate.equals(coordinate);
}
function isDestinationCoordinate(coordinate) {
return destinationNode && destinationNode.coordinate.equals(coordinate);
}
function isCoordinateOnGrid(coordinate) {
return coordinate.x >= 0 && coordinate.x < GRID_WIDTH &&
coordinate.y >= 0 && coordinate.y < GRID_HEIGHT;
}
function canPlaceCFC(node) {
return node.connectedNodes.length <= 1;
}
function getGridItem(globalX, globalY) {
var paperPosition = paper.position();
var x = globalX - paperPosition.left + paper.scrollLeft() + PAPER_PADDING;
var y = globalY - paperPosition.top + paper.scrollTop() + PAPER_PADDING;
x /= GRID_SPACE_SIZE;
y /= GRID_SPACE_SIZE;
x = Math.min(Math.max(0, Math.floor(x)), GRID_WIDTH - 1);
y = Math.min(Math.max(0, Math.floor(y)), GRID_HEIGHT - 1);
return grid[x][y];
}
/*************/
/* Rendering */
/*************/
function initialiseGrid() {
grid = drawing.createGrid();
for (var i = 0; i < grid.length; i++) {
for (var j = 0; j < grid[i].length; j++) {
grid[i][j].node.onmousedown = handleMouseDown(grid[i][j]);
grid[i][j].node.onmouseover = handleMouseOver(grid[i][j]);
grid[i][j].node.onmouseout = handleMouseOut(grid[i][j]);
grid[i][j].node.onmouseup = handleMouseUp(grid[i][j]);
grid[i][j].node.ontouchstart = handleTouchStart(grid[i][j]);
grid[i][j].node.ontouchmove = handleTouchMove(grid[i][j]);
grid[i][j].node.ontouchend = handleTouchEnd(grid[i][j]);
}
}
}
function clearLevelNameInputInSaveTab() {
$('#levelNameInput').val('');
}
function clear() {
for (var i = trafficLights.length-1; i >= 0; i--) {
trafficLights[i].destroy();
}
for (var i = decor.length-1; i >= 0; i--) {
decor[i].destroy();
}
for (var i = cows.length-1; i >= 0; i--) {
cows[i].destroy();
}
nodes = [];
strikeStart = null;
originNode = null;
destinationNode = null;
cowGroups = {};
currentCowGroupId = 1;
$('#cow_group_select').find('option').remove();
// Add initial cow group
addCowGroup();
clearLevelNameInputInSaveTab();
}
function drawAll() {
drawing.renderGrid(grid, currentTheme);
redrawRoad();
}
function redrawRoad() {
drawing.renderRoad(nodes);
clearMarkings();
bringTrafficLightsToFront();
bringDecorToFront();
}
function bringDecorToFront() {
for (var i = 0; i < decor.length; i++) {
decor[i].image.toFront();
}
}
function bringTrafficLightsToFront() {
for (var i = 0; i < trafficLights.length; i++) {
trafficLights[i].image.toFront();
}
}
function bringCowsToFront() {
for (var i = 0; i < cows.length; i++) {
cows[i].image.toFront();
}
}
/************/
/* Marking */
/************/
// Methods for highlighting squares
function mark(coordMap, colour, opacity, occupied) {
var coordPaper = ocargo.Drawing.translate(coordMap);
var element = grid[coordPaper.x][coordPaper.y];
element.attr({fill:colour, "fill-opacity": opacity});
}
function markAsOrigin(coordinate) {
mark(coordinate, 'red', 0.7, true);
}
function markAsDestination(coordinate) {
mark(coordinate, 'blue', 0.7, true);
}
function markAsBackground(coordinate) {
mark(coordinate, currentTheme.background, 0, false);
}
function markAsSelected(coordinate) {
mark(coordinate, currentTheme.selected, 1, true);
}
function markAsHighlighted(coordinate) {
mark(coordinate, currentTheme.selected, 0.3, true);
}
function markCowNodes() {
if (cows) {
for (var i = 0; i < cows.length; i++) {
var internalCow = cows[i];
if (internalCow.controlledNode) {
mark(internalCow.controlledNode.coordinate, internalCow.data.group.color, 0.3, true);
}
}
}
}
function clearMarkings() {
for (var i = 0; i < GRID_WIDTH; i++) {
for (var j = 0; j < GRID_HEIGHT; j++) {
markAsBackground(new ocargo.Coordinate(i,j));
grid[i][j].toFront();
}
}
markCowNodes();
if (originNode) {
markAsOrigin(originNode.coordinate);
}
if (destinationNode) {
markAsDestination(destinationNode.coordinate);
}
bringTrafficLightsToFront();
bringCowsToFront();
bringDecorToFront();
}
function markTentativeRoad(currentEnd) {
clearMarkings();
applyAlongStrike(setup, currentEnd);
var previousNode = null;
function setup(x, y) {
var coordinate = new ocargo.Coordinate(x, y);
var node = new ocargo.Node(coordinate);
if (previousNode) {
node.addConnectedNodeWithBacklink(previousNode);
}
previousNode = node;
markAsSelected(coordinate);
}
}
/***************************/
/* Paper interaction logic */
/***************************/
// Function for making an element "transparent" to mouse events
// e.g. decor, traffic lights, rubbish bin etc.
function addReleaseListeners(element) {
var lastGridItem;
element.onmouseover =
function(e) {
lastGridItem = getGridItem(e.pageX, e.pageY);
if(strikeStart) {
handleMouseOver(lastGridItem)(e);
}
};
element.onmousemove =
function(e) {
var item = getGridItem(e.pageX, e.pageY);
if(item != lastGridItem) {
if(lastGridItem) {
handleMouseOut(lastGridItem)(e);
}
if(strikeStart) {
handleMouseOver(item)(e);
}
lastGridItem = item;
}
};
element.onmouseup =
function(e) {
if(strikeStart) {
handleMouseUp(getGridItem(e.pageX, e.pageY))(e);
}
};
// Touch events seem to have this behaviour automatically
}
function handleMouseDown(this_rect) {
return function (ev) {
ev.preventDefault();
var getBBox = this_rect.getBBox();
var coordPaper = getCoordinateFromBBox(getBBox);
var coordMap = ocargo.Drawing.translate(coordPaper);
var existingNode = ocargo.Node.findNodeByCoordinate(coordMap, nodes);
if (mode === modes.MARK_ORIGIN_MODE && existingNode && canPlaceCFC(existingNode)) {
if (originNode) {
var prevStart = originNode.coordinate;
markAsBackground(prevStart);
}
// Check if same as destination node
if (isDestinationCoordinate(coordMap)) {
destinationNode = null;
}
markAsOrigin(coordMap);
var newStartIndex = ocargo.Node.findNodeIndexByCoordinate(coordMap, nodes);
// Putting the new start in the front of the nodes list.
var temp = nodes[newStartIndex];
nodes[newStartIndex] = nodes[0];
nodes[0] = temp;
originNode = nodes[0];
} else if (mode === modes.MARK_DESTINATION_MODE && existingNode) {
if (destinationNode) {
var prevEnd = destinationNode.coordinate;
markAsBackground(prevEnd);
}
// Check if same as starting node
if (isOriginCoordinate(coordMap)) {
originNode = null;
}
markAsDestination(coordMap);
var newEnd = ocargo.Node.findNodeIndexByCoordinate(coordMap, nodes);
destinationNode = nodes[newEnd];
} else if (mode === modes.ADD_ROAD_MODE || mode === modes.DELETE_ROAD_MODE) {
strikeStart = coordMap;
markAsSelected(coordMap);
}
};
}
function getCoordinateFromBBox(bBox){
return new ocargo.Coordinate((bBox.x - PAPER_PADDING) / GRID_SPACE_SIZE, (bBox.y - PAPER_PADDING) / GRID_SPACE_SIZE);
}
function handleMouseOver(this_rect) {
return function(ev) {
ev.preventDefault();
var getBBox = this_rect.getBBox();
var coordPaper = getCoordinateFromBBox(getBBox);
var coordMap = ocargo.Drawing.translate(coordPaper);
if (mode === modes.ADD_ROAD_MODE || mode === modes.DELETE_ROAD_MODE) {
if (strikeStart !== null) {
markTentativeRoad(coordMap);
} else if (!isOriginCoordinate(coordMap) && !isDestinationCoordinate(coordMap)) {
markAsHighlighted(coordMap);
}
} else if (mode === modes.MARK_ORIGIN_MODE || mode === modes.MARK_DESTINATION_MODE) {
var node = ocargo.Node.findNodeByCoordinate(coordMap, nodes);
if (node && destinationNode !== node && originNode !== node) {
if (mode === modes.MARK_DESTINATION_MODE) {
mark(coordMap, 'blue', 0.3, true);
} else if (canPlaceCFC(node)) {
mark(coordMap, 'red', 0.5, true);
}
}
}
};
}
function handleMouseOut(this_rect) {
return function(ev) {
ev.preventDefault();
var getBBox = this_rect.getBBox();
var coordPaper = getCoordinateFromBBox(getBBox);
var coordMap = ocargo.Drawing.translate(coordPaper);
if (mode === modes.MARK_ORIGIN_MODE || mode === modes.MARK_DESTINATION_MODE) {
var node = ocargo.Node.findNodeByCoordinate(coordMap, nodes);
if (node && destinationNode !== node && originNode !== node) {
markAsBackground(coordMap);
markCowNodes();
}
} else if (mode === modes.ADD_ROAD_MODE || mode === modes.DELETE_ROAD_MODE) {
if (!isOriginCoordinate(coordMap) && !isDestinationCoordinate(coordMap)) {
markAsBackground(coordMap);
markCowNodes();
}
}
};
}
function handleMouseUp(this_rect) {
return function(ev) {
ev.preventDefault();
if (mode === modes.ADD_ROAD_MODE || mode === modes.DELETE_ROAD_MODE) {
var getBBox = this_rect.getBBox();
var coordPaper = getCoordinateFromBBox(getBBox);
var coordMap = ocargo.Drawing.translate(coordPaper);
if (mode === modes.DELETE_ROAD_MODE) {
finaliseDelete(coordMap);
} else {
finaliseMove(coordMap);
}
sortNodes(nodes);
redrawRoad();
}
};
}
function handleTouchStart() {
return function (ev) {
if (ev.touches.length === 1 && !isScrolling) {
var gridItem = getGridItem(ev.touches[0].pageX, ev.touches[0].pageY);
handleMouseDown(gridItem)(ev);
}
};
}
function handleTouchMove() {
return function(ev) {
if (ev.touches.length === 1 && !isScrolling) {
var gridItem = getGridItem(ev.touches[0].pageX, ev.touches[0].pageY);
handleMouseOver(gridItem)(ev);
}
};
}
function handleTouchEnd() {
return function(ev) {
if (ev.changedTouches.length === 1 && !isScrolling) {
var gridItem = getGridItem(ev.changedTouches[0].pageX, ev.changedTouches[0].pageY);
handleMouseUp(gridItem)(ev);
}
};
}
function setupDecorListeners(decor) {
var image = decor.image;
var originX;
var originY;
var paperX;
var paperY;
var paperWidth;
var paperHeight;
var imageWidth;
var imageHeight;
function onDragMove(dx, dy) {
paperX = dx + originX;
paperY = dy + originY;
// Deal with trashcan
checkImageOverTrashcan(paperX, paperY, imageWidth, imageHeight);
// Stop it being dragged off the edge of the page
if (paperX < 0) {
paperX = 0;
} else if (paperX + imageWidth > paperWidth) {
paperX = paperWidth - imageWidth;
}
if (paperY < 0) {
paperY = 0;
} else if (paperY + imageHeight > paperHeight) {
paperY = paperHeight - imageHeight;
}
image.transform('t' + paperX + ',' + paperY);
}
function onDragStart(x, y) {
var bBox = image.getBBox();
imageWidth = bBox.width;
imageHeight = bBox.height;
var paperPosition = paper.position();
originX = x - paperPosition.left + paper.scrollLeft() - imageWidth/2;
originY = y - paperPosition.top + paper.scrollTop() - imageHeight/2;
paperWidth = GRID_WIDTH * GRID_SPACE_SIZE + PAPER_PADDING;
paperHeight = GRID_HEIGHT * GRID_SPACE_SIZE + PAPER_PADDING;
}
function onDragEnd() {
originX = paperX;
originY = paperY;
if(trashcanOpen) {
decor.destroy();
}
closeTrashcan();
}
image.drag(onDragMove, onDragStart, onDragEnd);
addReleaseListeners(image.node);
}
function setupCowListeners(cow) {
var image = cow.image;
// Position in map coordinates.
var controlledCoord;
// Current position of the element in paper coordinates
var paperX;
var paperY;
// Where the drag started in paper coordinates
var originX;
var originY;
// Size of the paper
var paperWidth;
var paperHeight;
// Size of the image
var imageWidth;
var imageHeight;
var moved = false;
function onDragMove(dx, dy) {
cow.valid = false;
image.attr({'cursor':'default'});
moved = dx !== 0 || dy !== 0;
// Update image's position
paperX = dx + originX;
paperY = dy + originY;
// Trashcan check
checkImageOverTrashcan(paperX, paperY, imageWidth, imageHeight);
// Stop it being dragged off the edge of the page
if (paperX < 0) {
paperX = 0;
} else if (paperX + imageWidth > paperWidth) {
paperX = paperWidth - imageWidth;
}
if (paperY < 0) {
paperY = 0;
} else if (paperY + imageHeight > paperHeight) {
paperY = paperHeight - imageHeight;
}
// And perform the updatee
image.transform('t' + paperX + ',' + paperY );
//Unmark the squares the cow previously occupied
if (controlledCoord) {
markAsBackground(controlledCoord);
}
if(cows) {
for( var i = 0; i < cows.length; i++){
var internalCow = cows[i];
if(internalCow !== cow && internalCow.controlledNode) {
mark(internalCow.controlledNode.coordinate, internalCow.data.group.color, 0.3, true);
}
}
}
if (originNode) {
markAsOrigin(originNode.coordinate);
}
if (destinationNode) {
markAsDestination(destinationNode.coordinate);
}
// Now calculate the source coordinate
var box = image.getBBox();
var absX = (box.x + box.width/2) / GRID_SPACE_SIZE;
var absY = (box.y + box.height/2) / GRID_SPACE_SIZE;
var x = Math.min(Math.max(0, Math.floor(absX)), GRID_WIDTH - 1);
var y = GRID_HEIGHT - Math.min(Math.max(0, Math.floor(absY)), GRID_HEIGHT - 1) - 1;
controlledCoord = new ocargo.Coordinate(x,y);
// If source node is not on grid remove it
if (!isCoordinateOnGrid(controlledCoord)) {
controlledCoord = null;
}
if (controlledCoord) {
var colour;
if(isValidPlacement(controlledCoord)) {
colour = VALID_LIGHT_COLOUR;
} else {
colour = INVALID_LIGHT_COLOUR;
}
mark(controlledCoord, colour, 0.7, false);
}
// Deal with trashcan
var paperAbsX = paperX - paper.scrollLeft() + imageWidth/2;
var paperAbsY = paperY - paper.scrollTop() + imageHeight/2;
var trashcanWidth = $('#trashcanHolder').width();
var trashcanHeight = $('#trashcanHolder').height();
if(paperAbsX > trashcanAbsolutePaperX && paperAbsX <= trashcanAbsolutePaperX + trashcanWidth &&
paperAbsY > trashcanAbsolutePaperY - 20 && paperAbsY <= trashcanAbsolutePaperY + trashcanHeight) {
openTrashcan();
} else {
closeTrashcan();
}
}
function onDragStart(x, y) {
var bBox = image.getBBox();
imageWidth = bBox.width;
imageHeight = bBox.height;
var paperPosition = paper.position();
originX = x - paperPosition.left + paper.scrollLeft() - imageWidth/2;
originY = y - paperPosition.top + paper.scrollTop() - imageHeight/2;
paperWidth = GRID_WIDTH * GRID_SPACE_SIZE;
paperHeight = GRID_HEIGHT * GRID_SPACE_SIZE;
adjustCowGroupMinMaxFields(cow);
}
function onDragEnd() {
//Unmark previously occupied square
if(cow.controlledNode) {
markAsBackground(cow.controlledNode.coordinate);
}
// Mark squares currently occupied
if (controlledCoord) {
mark(controlledCoord, cow.data.group.color, 0.3, true);
}
if (originNode) {
markAsOrigin(originNode.coordinate);
}
if (destinationNode) {
markAsDestination(destinationNode.coordinate);
}
if(trashcanOpen) {
cow.destroy();
} else if(isValidPlacement(controlledCoord)) {
// Add back to the list of cows if on valid nodes
var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
cow.controlledNode = controlledNode;
cow.valid = true;
drawing.setCowImagePosition(controlledCoord, image, controlledNode);
} else {
cow.controlledNode = null;
cow.valid = false;
}
adjustCowGroupMinMaxFields(cow);
image.attr({'cursor':'pointer'});
closeTrashcan();
}
function isValidPlacement(controlledCoord){
var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
if (!controlledNode)
return false;
for (var i=0; i < cows.length; i++) {
var otherCow = cows[i];
if (otherCow.controlledNode == controlledNode && cow != otherCow)
return false;
}
return true;
}
image.drag(onDragMove, onDragStart, onDragEnd);
addReleaseListeners(image.node);
}
function adjustCowGroupMinMaxFields(draggedCow) {
var draggedCowGroupId = draggedCow.data.group.id;
var noOfValidCowsInGroup = 0;
for (var i=0; i < cows.length; i++) {
if(cows[i].valid && cows[i].data.group.id === draggedCowGroupId) {
noOfValidCowsInGroup++;
}
}
var draggedCowGroup = cowGroups[draggedCowGroupId];
draggedCowGroup.minCows = Math.max(1, Math.min(draggedCowGroup.minCows, noOfValidCowsInGroup));
draggedCowGroup.maxCows = Math.max(1, Math.min(draggedCowGroup.maxCows, noOfValidCowsInGroup));
$('#cow_group_select').val(draggedCowGroupId).change();
}
function setupTrafficLightListeners(trafficLight) {
var image = trafficLight.image;
// Position in map coordinates.
var sourceCoord;
var controlledCoord;
// Current position of the element in paper coordinates
var paperX;
var paperY;
// Where the drag started in paper coordinates
var originX;
var originY;
// Size of the paper
var paperWidth;
var paperHeight;
// Size of the image
var imageWidth;
var imageHeight;
// Orientation and rotation transformations
var scaling;
var rotation;
var moved = false;
function onDragMove(dx, dy) {
trafficLight.valid = false;
image.attr({'cursor':'default'});
moved = dx !== 0 || dy !== 0;
// Update image's position
paperX = dx + originX;
paperY = dy + originY;
// Adjust for the fact that we've rotated the image
if (rotation === 90 || rotation === 270) {
paperX += (imageWidth - imageHeight)/2;
paperY -= (imageWidth - imageHeight)/2;
}
// Trashcan check
checkImageOverTrashcan(paperX, paperY, imageWidth, imageHeight);
// Stop it being dragged off the edge of the page
if (paperX < 0) {
paperX = 0;
} else if (paperX + imageWidth > paperWidth) {
paperX = paperWidth - imageWidth;
}
if (paperY < 0) {
paperY = 0;
} else if (paperY + imageHeight > paperHeight) {
paperY = paperHeight - imageHeight;
}
// And perform the updatee
image.transform('t' + paperX + ',' + paperY + 'r' + rotation + 's' + scaling);
// Unmark the squares the light previously occupied
if (sourceCoord) {
markAsBackground(sourceCoord);
}
if (controlledCoord) {
markAsBackground(controlledCoord);
}
markCowNodes();
if (originNode) {
markAsOrigin(originNode.coordinate);
}
if (destinationNode) {
markAsDestination(destinationNode.coordinate);
}
// Now calculate the source coordinate
var box = image.getBBox();
var absX = (box.x + box.width/2) / GRID_SPACE_SIZE;
var absY = (box.y + box.height/2) / GRID_SPACE_SIZE;
switch(rotation) {
case 0:
absY += 0.5;
break;
case 90:
absX -= 0.5;
break;
case 180:
absY -= 0.5;
break;
case 270:
absX += 0.5;
break;
}
var x = Math.min(Math.max(0, Math.floor(absX)), GRID_WIDTH - 1);
var y = GRID_HEIGHT - Math.min(Math.max(0, Math.floor(absY)), GRID_HEIGHT - 1) - 1;
sourceCoord = new ocargo.Coordinate(x,y);
// Find controlled position in map coordinates
switch(rotation) {
case 0:
controlledCoord = new ocargo.Coordinate(sourceCoord.x, sourceCoord.y + 1);
break;
case 90:
controlledCoord = new ocargo.Coordinate(sourceCoord.x + 1, sourceCoord.y);
break;
case 180:
controlledCoord = new ocargo.Coordinate(sourceCoord.x, sourceCoord.y - 1);
break;
case 270:
controlledCoord = new ocargo.Coordinate(sourceCoord.x - 1, sourceCoord.y);
break;
}
// If controlled node is not on grid, remove it
if (!isCoordinateOnGrid(controlledCoord)) {
controlledCoord = null;
}
// If source node is not on grid remove it
if (!isCoordinateOnGrid(sourceCoord)) {
sourceCoord = null;
}
if (sourceCoord && controlledCoord) {
var colour;
if(isValidPlacement(sourceCoord, controlledCoord)) {
colour = VALID_LIGHT_COLOUR;
drawing.setTrafficLightImagePosition(sourceCoord, controlledCoord, image);
} else {
colour = INVALID_LIGHT_COLOUR;
}
mark(controlledCoord, colour, 0.7, false);
mark(sourceCoord, colour, 0.7, false);
}
// Deal with trashcan
var paperAbsX = paperX - paper.scrollLeft() + imageWidth/2;
var paperAbsY = paperY - paper.scrollTop() + imageHeight/2;
var trashcanWidth = $('#trashcanHolder').width();
var trashcanHeight = $('#trashcanHolder').height();
if(paperAbsX > trashcanAbsolutePaperX && paperAbsX <= trashcanAbsolutePaperX + trashcanWidth &&
paperAbsY > trashcanAbsolutePaperY - 20 && paperAbsY <= trashcanAbsolutePaperY + trashcanHeight) {
openTrashcan();
} else {
closeTrashcan();
}
}
function onDragStart(x, y) {
moved = false;
scaling = getScaling(image);
rotation = (image.matrix.split().rotate + 360) % 360;
var bBox = image.getBBox();
imageWidth = bBox.width;
imageHeight = bBox.height;
paperWidth = GRID_WIDTH * GRID_SPACE_SIZE + PAPER_PADDING;
paperHeight = GRID_HEIGHT * GRID_SPACE_SIZE + PAPER_PADDING;
var paperPosition = paper.position();
var mouseX = x - paperPosition.left;
var mouseY = y - paperPosition.top;
originX = mouseX + paper.scrollLeft()- imageWidth/2;
originY = mouseY + paper.scrollTop() - imageHeight/2;
}
function onDragEnd() {
// Unmark squares currently occupied
if (sourceCoord) {
markAsBackground(sourceCoord);
}
if (controlledCoord) {
markAsBackground(controlledCoord);
}
markCowNodes();
if (originNode) {
markAsOrigin(originNode.coordinate);
}
if (destinationNode) {
markAsDestination(destinationNode.coordinate);
}
if(trashcanOpen) {
trafficLight.destroy();
} else if(isValidPlacement(sourceCoord, controlledCoord)) {
// Add back to the list of traffic lights if on valid nodes
trafficLight.sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoord, nodes);
trafficLight.controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
trafficLight.valid = true;
drawing.setTrafficLightImagePosition(sourceCoord, controlledCoord, image);
}
image.attr({'cursor':'pointer'});
closeTrashcan();
}
image.drag(onDragMove, onDragStart, onDragEnd);
addReleaseListeners(image.node);
var myLatestTap;
$(image.node).on('click touchstart', function() {
var now = new Date().getTime();
var timesince = now - myLatestTap;
if ((timesince < 300) && (timesince > 0)) {
image.transform('...r90');
}
myLatestTap = new Date().getTime();
});
function getScaling(object) {
var transform = object.transform();
for (var i = 0; i < transform.length; i++) {
if (transform[i][0] === 's') {
return transform[i][1] + ',' + transform[i][2];
}
}
return "0,0";
}
function isValidPlacement(sourceCoord, controlledCoord) {
var sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoord, nodes);
var controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoord, nodes);
// Test if two connected nodes exist
var connected = false;
if (sourceNode && controlledNode) {
for (var i = 0; i < sourceNode.connectedNodes.length; i++) {
if (sourceNode.connectedNodes[i] === controlledNode) {
connected = true;
break;
}
}
}
if(!connected) {
return false;
}
// Test it's not already occupied
for(var i = 0; i < trafficLights.length; i++) {
var tl = trafficLights[i];
if(tl.valid &&
((tl.sourceNode === sourceNode && tl.controlledNode === controlledNode) ||
(tl.sourceNode === controlledNode && tl.controlledNode === sourceNode))) {
return false;
}
}
return true;
}
function occupied(sourceCoord, controlledCoord) {
}
}
/********************************/
/* Miscaellaneous state methods */
/********************************/
function finaliseDelete(strikeEnd) {
function deleteNode(x, y) {
var coord = new ocargo.Coordinate(x, y);
var node = ocargo.Node.findNodeByCoordinate(coord, nodes);
if (node) {
// Remove all the references to the node we're removing.
for (var i = node.connectedNodes.length - 1; i >= 0; i--) {
node.removeDoublyConnectedNode(node.connectedNodes[i]);
}
nodes.splice(nodes.indexOf(node), 1);
// Check if start or destination node
if (isOriginCoordinate(coord)) {
markAsBackground(originNode.coordinate);
originNode = null;
}
if (isDestinationCoordinate(coord)) {
markAsBackground(destinationNode.coordinate);
destinationNode = null;
}
// Check if any traffic lights present
for (var i = trafficLights.length-1; i >= 0; i--) {
var trafficLight = trafficLights[i];
if (node === trafficLight.sourceNode || node === trafficLight.controlledNode) {
trafficLights.splice(i, 1);
trafficLight.destroy();
}
}
// Check if any cows present
for (var i = cows.length-1; i >= 0; i--) {
var cow = cows[i];
if (node === cow.controlledNode) {
cows.splice(i, 1);
cow.destroy();
}
}
}
}
applyAlongStrike(deleteNode, strikeEnd);
strikeStart = null;
// Delete any nodes isolated through deletion
for (var i = nodes.length - 1; i >= 0; i--) {
if (nodes[i].connectedNodes.length === 0) {
var coordinate = nodes[i].coordinate;
deleteNode(coordinate.x, coordinate.y);
}
}
}
function finaliseMove(strikeEnd) {
var previousNode = null;
function addNode(x, y) {
var coord = new ocargo.Coordinate(x,y);
var node = ocargo.Node.findNodeByCoordinate(coord, nodes);
if (!node) {
node = new ocargo.Node(coord);
nodes.push(node);
}
// Now connect it up with it's new neighbours
if (previousNode && node.connectedNodes.indexOf(previousNode) === -1) {
node.addConnectedNodeWithBacklink(previousNode);
// If we've overwritten the origin node remove it as
// we can no longer place the CFC there
if (node === originNode || previousNode == originNode) {
markAsBackground(originNode.coordinate);
originNode = null;
}
}
previousNode = node;
}
if(strikeStart && !(strikeStart.x === strikeEnd.x && strikeStart.y === strikeEnd.y)) {
applyAlongStrike(addNode, strikeEnd);
}
strikeStart = null;
}
function applyAlongStrike(func, strikeEnd) {
var x, y;
if (!strikeStart) {
return;
}
if (strikeStart.x <= strikeEnd.x) {
for (x = strikeStart.x; x <= strikeEnd.x; x++) {
func(x, strikeStart.y);
}
} else {
for (x = strikeStart.x; x >= strikeEnd.x; x--) {
func(x, strikeStart.y);
}
}
if (strikeStart.y <= strikeEnd.y) {
for (y = strikeStart.y + 1; y <= strikeEnd.y; y++) {
func(strikeEnd.x, y);
}
} else {
for (y = strikeStart.y - 1; y >= strikeEnd.y; y--) {
func(strikeEnd.x, y);
}
}
}
function findTrafficLight(firstIndex, secondIndex) {
var light;
for (var i = 0; i < trafficLights.length; i++) {
light = trafficLights[i];
if (light.node === firstIndex && light.sourceNode === secondIndex) {
return i;
}
}
return -1;
}
function setTheme(theme) {
currentTheme = theme;
for (var x = 0; x < GRID_WIDTH; x++) {
for (var y = 0; y < GRID_HEIGHT; y++) {
grid[x][y].attr({stroke: theme.border});
}
}
for (var i = 0; i < decor.length; i++) {
decor[i].updateTheme();
}
$('.decor_button').each(function(index, element) {
element.src = theme.decor[element.id].url;
});
$('#paper').css({'background-color': theme.background});
}
function sortNodes(nodes) {
var sorter = function(a, b) {
return comparator(a, b, nodes[i]);
};
for (var i = 0; i < nodes.length; i++) {
// Remove duplicates.
var newConnected = [];
for (var j = 0; j < nodes[i].connectedNodes.length; j++) {
if (newConnected.indexOf(nodes[i].connectedNodes[j]) === -1) {
newConnected.push(nodes[i].connectedNodes[j]);
}
}
nodes[i].connectedNodes.sort(sorter).reverse();
}
function comparator(node1, node2, centralNode) {
var a1 = ocargo.calculateNodeAngle(centralNode, node1);
var a2 = ocargo.calculateNodeAngle(centralNode, node2);
if (a1 < a2) {
return -1;
} else if (a1 > a2) {
return 1;
} else {
return 0;
}
}
}
/**********************************/
/* Loading/saving/sharing methods */
/**********************************/
function extractState() {
var state = {};
// Create node data
sortNodes(nodes);
state.path = JSON.stringify(ocargo.Node.composePathData(nodes));
// Create traffic light data
var trafficLightData = [];
var i;
for (i = 0; i < trafficLights.length; i++) {
var tl = trafficLights[i];
if (tl.valid) {
trafficLightData.push(tl.getData());
}
}
state.traffic_lights = JSON.stringify(trafficLightData);
var cowsData = [];
var cowGroupData = {};
for( var i = 0; i < cows.length; i++){
if(cows[i].valid) {
var groupId = cows[i].data.group.id;
if(!cowGroupData[groupId]) {
cowGroupData[groupId] = {minCows : cowGroups[groupId].minCows,
maxCows : cowGroups[groupId].maxCows,
potentialCoordinates : [],
type: cowGroups[groupId].type}; //editor can only add white cow for now
}
var coordinates = cows[i].controlledNode.coordinate;
var strCoordinates = {'x':coordinates.x, 'y':coordinates.y};
cowGroupData[groupId].potentialCoordinates.push(strCoordinates);
}
}
for(var groupId in cowGroupData) {
cowsData.push(cowGroupData[groupId]);
}
state.cows = JSON.stringify(cowsData);
// Create block data
state.blocks = [];
for (i = 0; i < BLOCKS.length; i++) {
var type = BLOCKS[i];
if ($('#' + type + "_checkbox").is(':checked')) {
var block = {'type': type};
var number = $('#' + type + "_number").val();
if(number !== "infinity") {
block.number = parseInt(number);
}
state.blocks.push(block);
}
}
// Create decor data
state.decor = [];
for (i = 0; i < decor.length; i++) {
state.decor.push(decor[i].getData());
}
state.decor = ocargo.utils.sortObjects(state.decor, "z");
// Destination and origin data
if (destinationNode) {
var destinationCoord = destinationNode.coordinate;
state.destinations = JSON.stringify([[destinationCoord.x, destinationCoord.y]]);
}
if (originNode) {
var originCoord = originNode.coordinate;
var nextCoord = originNode.connectedNodes[0].coordinate;
var direction = originCoord.getDirectionTo(nextCoord);
state.origin = JSON.stringify({coordinate: [originCoord.x, originCoord.y], direction: direction});
}
// Starting fuel of the level
state.max_fuel = $('#max_fuel').val();
// Language data
var language = $('#language_select').val();
state.blocklyEnabled = language === 'blockly' || language === 'both' || language === 'blocklyWithPythonView';
state.pythonViewEnabled = language === 'blocklyWithPythonView';
state.pythonEnabled = language === 'python' || language === 'both';
// Other data
state.theme = currentTheme.id;
state.character = $('#character_select').val();
return state;
}
function restoreState(state) {
clear();
// Load node data
nodes = ocargo.Node.parsePathData(JSON.parse(state.path));
// Load traffic light data
var trafficLightData = JSON.parse(state.traffic_lights);
for (var i = 0; i < trafficLightData.length; i++) {
new InternalTrafficLight(trafficLightData[i]);
}
if(COW_LEVELS_ENABLED) {
var cowGroupData = JSON.parse(state.cows);
for (var i = 0; i < cowGroupData.length; i++) {
// Add new group to group select element
if (i >= Object.keys(cowGroups).length) {
addCowGroup();
}
var cowGroupId = Object.keys(cowGroups)[i];
cowGroups[cowGroupId].minCows = cowGroupData[i].minCows;
cowGroups[cowGroupId].maxCows = cowGroupData[i].maxCows;
cowGroups[cowGroupId].type = cowGroupData[i].type;
if (cowGroupData[i].potentialCoordinates != null) {
for (var j = 0; j < cowGroupData[i].potentialCoordinates.length; j++) {
var cowData = {
coordinates: [cowGroupData[i].potentialCoordinates[j]],
group: cowGroups[cowGroupId]
};
new InternalCow(cowData);
}
}
}
// Trigger change listener on cow group select box to set initial min/max values
$('#cow_group_select').change();
markCowNodes();
}
// Load in destination and origin nodes
// TODO needs to be fixed in the long term with multiple destinations
if (state.destinations) {
var destination = JSON.parse(state.destinations)[0];
var destinationCoordinate = new ocargo.Coordinate(destination[0], destination[1]);
destinationNode = ocargo.Node.findNodeByCoordinate(destinationCoordinate, nodes);
}
if (state.origin) {
var origin = JSON.parse(state.origin);
var originCoordinate = new ocargo.Coordinate(origin.coordinate[0], origin.coordinate[1]);
originNode = ocargo.Node.findNodeByCoordinate(originCoordinate, nodes);
}
// Load in character
$('#character_select').val(state.character);
$('#character_select').change();
drawAll();
// Set the theme
var themeFound = false;
var themeID = state.theme;
for (var themeName in THEMES) {
var theme = THEMES[themeName];
if (theme.id === themeID) {
setTheme(theme);
themeFound = true;
$('#theme_select').val(themeName);
break;
}
}
if(!themeFound) {
setTheme(THEMES.grass);
}
// Load in the decor data
var decor = state.decor;
for (var i = 0; i < decor.length; i++) {
var decorObject = new InternalDecor(decor[i].decorName);
decorObject.setPosition(decor[i].x + PAPER_PADDING,
PAPER_HEIGHT - currentTheme.decor[decor[i].decorName].height - decor[i].y + PAPER_PADDING);
}
// Load in block data
if(state.blocks) {
for(var i = 0; i < BLOCKS.length; i++) {
var type = BLOCKS[i];
$('#' + type + '_checkbox').prop('checked', false);
$('#' + type + '_number').val('infinity');
}
var blocks = state.blocks;
for(var i = 0; i < blocks.length; i++) {
var type = blocks[i].type;
$('#' + type + '_checkbox').prop('checked', true);
if(blocks[i].number) {
$('#' + type + '_number').val(blocks[i].number);
}
}
}
// Load in language data
var languageSelect = $('#language_select');
if (state.blocklyEnabled && state.pythonViewEnabled){
languageSelect.val('blocklyWithPythonView');
} else if(state.blocklyEnabled && state.pythonEnabled) {
languageSelect.val('both');
} else if(state.pythonEnabled) {
languageSelect.val('python');
} else {
languageSelect.val('blockly');
}
languageSelect.change();
// Other data
if(state.max_fuel) {
$('#max_fuel').val(state.max_fuel);
}
}
function loadLevel(levelID) {
saving.retrieveLevel(levelID, function(err, level, owned) {
if (err !== null) {
console.error(err);
return;
}
restoreState(level, true);
saveState.loaded(owned, extractState(), level.id);
});
}
function saveLevel(name, levelId, callback) {
var level = extractState();
level.name = name;
ownedLevels.save(level, levelId, callback);
}
function isLevelValid() {
// Check to see if a road has been created
if (nodes === undefined || nodes.length == 0) {
var noRoad = interpolate(
gettext('In %(map_icon)s%(map_label)s menu, click on %(add_road_icon)s%(add_road_label)s. Draw a road by clicking ' +
'on a square then dragging to another square.'
), {
map_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/map.svg', 'popupIcon'),
map_label: '<b>' + gettext('Map') + '</b>',
add_road_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/add_road.svg', 'popupIcon'),
add_road_label: '<b>' + gettext('Add road') + '</b>'
},
true
);
ocargo.Drawing.startPopup(gettext('Oh no!'), gettext('You forgot to create a road.'), noRoad);
return false;
}
// Check to see if start and end nodes have been marked
if (!originNode || !destinationNode) {
var noStartOrEnd = interpolate(
gettext('In %(map_icon)s%(map_label)s menu, click on %(mark_start_icon)s%(mark_start_label)s or ' +
'%(mark_end_icon)s%(mark_end_label)s then select the square where you want the road to start or end.'
), {
map_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/map.svg', 'popupIcon'),
map_label: '<b>' + gettext('Map') + '</b>',
mark_start_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/origin.svg', 'popupIcon'),
mark_start_label: '<b>' + gettext('Mark start') + '</b>',
mark_end_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/destination.svg', 'popupIcon'),
mark_end_label: '<b>' + gettext('Mark end') + '</b>'
},
true
);
ocargo.Drawing.startPopup(gettext('Oh no!'), gettext('You forgot to mark the start and end points.'), noStartOrEnd);
return false;
}
// Check to see if path exists from start to end
if (!areDestinationsReachable(originNode, [destinationNode], nodes)) {
ocargo.Drawing.startPopup(gettext('Something is wrong...'),
gettext('There is no way to get from the start to the destination.'),
gettext('Edit your level to allow the driver to get to the end.'));
return false;
}
// Check to see if at least one block selected
// (not perfect but ensures that they don't think the blockly toolbar is broken)
if($(".block_checkbox:checked").length == 0) {
var noBlocks = interpolate(
gettext('Go to %(code_icon)s%(code_label)s and select some to use. Remember to include the move and turn ' +
'commands!'
), {
code_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/blockly.svg', 'popupIcon'),
code_label: '<b>' + gettext('Code') + '</b>'
},
true
);
ocargo.Drawing.startPopup(gettext('Something is wrong...'),
gettext('You haven\'t selected any blocks to use in your level'),
noBlocks);
return false;
}
return true;
}
function hasLevelChangedSinceSave() {
var currentState = JSON.stringify(extractState());
return saveState.hasChanged(currentState)
}
function canShare() {
if (!saveState.isSaved()) {
ocargo.Drawing.startPopup(gettext('Sharing'), '', gettext('Please save your level before continuing!'));
return false;
} else if (hasLevelChangedSinceSave()) {
ocargo.Drawing.startPopup(gettext('Sharing'), '', gettext('Please save your latest changes!'));
return false;
}
return true;
}
function isLevelOwned() {
if (!saveState.isOwned()) {
ocargo.Drawing.startPopup(gettext('Sharing'), '',
gettext('You do not own this level. If you would like to share it you will first have to save your own copy!')
);
return false;
}
return true;
}
function isLoggedIn(activity) {
if (USER_STATUS !== "SCHOOL_STUDENT" && USER_STATUS !== "TEACHER" && USER_STATUS !== "INDEPENDENT_STUDENT") {
var getNotLoggedInMessage = function() {
var notLoggedInMessages = [];
switch (activity) {
case 'save':
notLoggedInMessages.push(gettext('Unfortunately you need to be logged in to save levels.'));
break;
case 'load':
notLoggedInMessages.push(gettext('Unfortunately you need to be logged in to load levels.'));
break;
case 'share':
notLoggedInMessages.push(gettext('Unfortunately you need to be logged in to share levels.'));
break;
}
notLoggedInMessages.push(interpolate(gettext('You can log in as a %(student_login_url)s, '
+ '%(teacher_login_url)s or %(independent_login_url)s.'), {
student_login_url: '<a href="' + Urls.student_login_access_code() + '">' + pgettext('login_url', 'student') + '</a>',
teacher_login_url: '<a href="' + Urls.teacher_login() + '">' + pgettext('login_url', 'teacher') + '</a>',
independent_login_url: '<a href="' + Urls.independent_student_login() + '">'
+ pgettext('login_url', 'independent student') + '</a>'
}, true));
return notLoggedInMessages.join(' ');
};
ocargo.Drawing.startPopup(gettext('Not logged in'), '', getNotLoggedInMessage());
return false;
}
return true;
}
function isIndependentStudent() {
if (USER_STATUS === "INDEPENDENT_STUDENT") {
ocargo.Drawing.startPopup(gettext('Sharing as an independent student'), '', gettext(
'Sorry but as an independent student you\'ll need to join a school or club to share your levels with others.'
));
return false;
}
return true;
}
function getLevelTextForDjangoMigration() {
// Put a call to this function in restoreState and you should get a string
// you can copy and paste into a Django migration file
var state = extractState();
var boolFields = ["pythonEnabled", "blocklyEnabled", 'fuel_gauge', 'direct_drive'];
var stringFields = ['path', 'traffic_lights', 'cows', 'origin', 'destinations'];
var otherFields = ['max_fuel'];
var decor = null;
var blocks = null;
var string = "levelNUMBER = Level(\n";
string += "\t\tname='NUMBER',\n";
string += "\t\tdefault=True,\n";
for(var propertyName in state) {
if(propertyName === 'decor') {
decor = JSON.stringify(state[propertyName]);
} else if(propertyName === 'blocks') {
blocks = JSON.stringify(state[propertyName]);
} else if(propertyName === 'character') {
string += "\t\tcharacter=Character.objects.get(id='" + state[propertyName] + "'),\n";
} else if(propertyName === 'theme') {
string += "\t\ttheme=Theme.objects.get(id=" + state[propertyName] + "),\n";
} else if(stringFields.indexOf(propertyName) != -1) {
string += "\t\t" + propertyName + "='" + state[propertyName] + "',\n";
} else if(boolFields.indexOf(propertyName) != -1) {
string += "\t\t" + propertyName + "=" + (state[propertyName] ? "True" : "False") + ",\n";
} else if(otherFields.indexOf(propertyName) != -1) {
string += "\t\t" + propertyName + "=" + state[propertyName] + ",\n";
} else {
console.log("DISCARDING " + propertyName)
}
}
string += "\t\tmodel_solution=FILL_IN,\n";
string += "\t)\n";
string += "\tlevelNUMBER.save()\n";
string += "\tset_decor(levelNUMBER, json.loads('" + decor + "'))\n";
string += "\tset_blocks(levelNUMBER, json.loads('" + blocks + "'))\n";
console.log("Copy this to a Django Migration file:\n" + string);
return string;
}
/*****************************************/
/* Internal cow representation */
/*****************************************/
function InternalCow(data) {
this.data = data;
this.getData = function() {
if (!this.valid) {
throw "Error: cannot create actual cow from invalid internal cow!";
}
// Where the cow is placed.
var coordinates = this.controlledNode.coordinate;
var strCoordinates= {'x':coordinates.x, 'y':coordinates.y};
return { "coordinates": [strCoordinates],
"groupId" : this.data.group.id
};
};
this.setCoordinate = function(){
};
this.destroy = function() {
this.image.remove();
var index = cows.indexOf(this);
if (index !== -1) {
cows.splice(index, 1);
}
};
this.image = drawing.createCowImage(data.group.type);
this.valid = false;
if ( data.coordinates && data.coordinates.length > 0 ) {
var coordinates = new ocargo.Coordinate(data.coordinates[0].x, data.coordinates[0].y);
this.controlledNode = ocargo.Node.findNodeByCoordinate(coordinates, nodes);
if (this.controlledNode) {
this.valid = true;
drawing.setCowImagePosition(coordinates, this.image, this.controlledNode);
}
} else {
this.image.transform('...t' + (-paper.scrollLeft()) + ',' + paper.scrollTop());
}
setupCowListeners(this);
this.image.attr({'cursor':'pointer'});
cows.push(this);
}
/*****************************************/
/* Internal traffic light representation */
/*****************************************/
function InternalTrafficLight(data) {
// public methods
this.getData = function() {
if (!this.valid) {
throw "Error: cannot create actual traffic light from invalid internal traffic light!";
}
var sourceCoord = this.sourceNode.coordinate;
var sourceCoordinate = {'x':sourceCoord.x, 'y':sourceCoord.y};
var direction = sourceCoord.getDirectionTo(this.controlledNode.coordinate);
return {"redDuration": this.redDuration, "greenDuration": this.greenDuration,
"sourceCoordinate": sourceCoordinate, "direction": direction,
"startTime": this.startTime, "startingState": this.startingState};
};
this.destroy = function() {
this.image.remove();
var index = trafficLights.indexOf(this);
if (index !== -1) {
trafficLights.splice(index, 1);
}
};
// data
this.redDuration = data.redDuration;
this.greenDuration = data.greenDuration;
this.startTime = data.startTime;
this.startingState = data.startingState;
var imgStr = this.startingState === ocargo.TrafficLight.RED ? LIGHT_RED_URL : LIGHT_GREEN_URL;
this.image = drawing.createTrafficLightImage(imgStr);
this.image.transform('...s-1,1');
this.valid = false;
if (data.sourceCoordinate && data.direction) {
var sourceCoordinate = new ocargo.Coordinate(data.sourceCoordinate.x, data.sourceCoordinate.y);
var controlledCoordinate = sourceCoordinate.getNextInDirection(data.direction);
this.sourceNode = ocargo.Node.findNodeByCoordinate(sourceCoordinate, nodes);
this.controlledNode = ocargo.Node.findNodeByCoordinate(controlledCoordinate, nodes);
if (this.controlledNode && this.sourceNode) {
this.valid = true;
drawing.setTrafficLightImagePosition(this.sourceNode.coordinate, this.controlledNode.coordinate, this.image);
}
} else {
this.image.transform('...t' + (-paper.scrollLeft()) + ',' + paper.scrollTop());
}
setupTrafficLightListeners(this);
this.image.attr({'cursor':'pointer'});
trafficLights.push(this);
}
/*********************************/
/* Internal decor representation */
/*********************************/
function InternalDecor(decorName) {
// public methods
this.getData = function() {
var bBox = this.image.getBBox();
var data = {
'x': Math.floor(bBox.x) - PAPER_PADDING,
'y': PAPER_HEIGHT - bBox.height - Math.floor(bBox.y) + PAPER_PADDING,
'z': currentTheme.decor[this.decorName].z_index,
'decorName': this.decorName
};
return data;
};
this.setPosition = function(x, y) {
this.image.transform('t' + x + ',' + y);
};
this.updateTheme = function() {
var description = currentTheme.decor[this.decorName];
var newImage = drawing.createImage(description.url, 0, 0, description.width,
description.height);
if (this.image) {
newImage.transform(this.image.matrix.toTransformString());
this.image.remove();
}
this.image = newImage;
this.image.attr({'cursor':'pointer'});
setupDecorListeners(this);
};
this.destroy = function() {
this.image.remove();
var index = decor.indexOf(this);
if (index !== -1) {
decor.splice(index, 1);
}
};
// data
this.decorName = decorName;
this.image = null;
// setup
this.updateTheme();
this.setPosition(paper.scrollLeft(), paper.scrollTop());
decor.push(this);
}
};
/******************/
/* Initialisation */
/******************/
$(function() {
var editor = new ocargo.LevelEditor(LEVEL); // This seems unused but removing it breaks the editor page.
var subtitle = interpolate(
gettext('Click %(help_icon)s%(help_label)s for clues on getting started.'), {
help_icon: ocargo.jsElements.image(ocargo.Drawing.imageDir + 'icons/help.svg', 'popupHelp'),
help_label: '<b>' + gettext('Help') + '</b>'
},
true
);
if (LEVEL === null){
ocargo.Drawing.startPopup(gettext('Welcome to the Level editor!'), subtitle, '');
} else {
let buttons = '';
buttons += ocargo.button.dismissButtonHtml("edit_button", "Edit");
buttons += ocargo.button.redirectButtonHtml("play_button", Urls.levels() + "custom/" + LEVEL,"Play");
ocargo.Drawing.startPopup(
gettext('Welcome back!'),
gettext('Would you like to edit or play with your design?'),
'',
false,
buttons,
);
}
});