42 | Upload an aerial or satellite photo from your city - an intersection or neighbourhood - and start mapping how much space is allocated to cars, pedestrians and bikes.
43 |
44 |
45 |
62 |
63 |
64 |
65 |
66 |
67 |
Select a color tool.
68 |
Click and drag to draw the color onto the map.
69 |
Right click to add a marker (e.g. cyclist/ped counts.)
70 |
Scroll or use your mouse wheel to cycle through colors.
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/arrogance.js:
--------------------------------------------------------------------------------
1 | var grid_width = 900;
2 | var grid_block_size = 40;
3 | var prev_grid_block_size = 40;
4 | var grid_block_number_x = grid_width / grid_block_size;
5 | var grid_block_number_y = grid_width / grid_block_size;
6 | var current_opacity = 0.5;
7 | var background_img = new Image();
8 | var grid = [];
9 | var markers = [];
10 | var tools = {
11 | cars: {
12 | color: 'rgba(209,34,38,0.5)',
13 | desc: 'Cars',
14 | markers: true // allow markers for this tool?
15 | },
16 | pedestrians: {
17 | color: 'rgba(22,169,227,0.5)',
18 | desc: 'Pedestrians',
19 | markers: true
20 | },
21 | cyclists: {
22 | color: 'rgba(150,79,160,0.5)',
23 | desc: 'Cyclists',
24 | markers: true
25 | },
26 | publictransport: {
27 | color: 'rgba(0,85,255,0.5)',
28 | desc: 'Public transport',
29 | markers: true
30 | },
31 | buildings: {
32 | color: 'rgba(255,255,100,0.5)',
33 | desc: 'Buildings',
34 | markers: false
35 | },
36 | green: {
37 | color: 'rgba(0,255,0,0.5)',
38 | desc: 'Green',
39 | markers: true
40 | },
41 | dead_space: {
42 | color: 'rgba(148,148,153,0.5)',
43 | desc: '"Dead" space',
44 | markers: false
45 | },
46 | eraser: {
47 | color: 'rgba(255,255,255,0.5)',
48 | desc: 'Eraser',
49 | markers: false
50 | }
51 | };
52 | var tools_keys = Object.keys(tools);
53 | var tools_length = Object.keys(tools).length;
54 | var selected_tool = tools_keys[0];
55 | var erase_key = tools_length - 1;
56 | var canvas = document.getElementById('canvas');
57 | var context = canvas.getContext("2d");
58 | var virtual_canvas = document.getElementById('virtual');
59 | var virtual_context = virtual_canvas.getContext('2d');
60 | $('#tool').text(tools[selected_tool].desc);
61 | $('#tool').css('background-color', tools[selected_tool].color);
62 |
63 |
64 | function initialize() {
65 | $('#container').css('max-width', $('#editor').css('width'));
66 | $('#container').css('max-height', $('#editor').css('height'));
67 | $('#container').css('width', $('#editor').css('width'));
68 | $('#container').css('height', $('#editor').css('height'));
69 | $('#canvas').attr('width', $('#container').css('width'));
70 | $('#canvas').attr('height', $('#container').css('height'));
71 | grid_width = parseInt($('#container').css('width'), 10);
72 | grid_height = parseInt($('#container').css('height'), 10);
73 | setup();
74 | }
75 |
76 | function setup() {
77 | $('#container').css('width', grid_width);
78 | $('#container').css('height', grid_height);
79 | $('#canvas').attr('width', $('#container').css('width'));
80 | $('#canvas').attr('height', $('#container').css('height'));
81 | grid_block_size = $('#gridblocksize').val() * 1;
82 | grid_block_number_x = Math.floor(grid_width / grid_block_size);
83 | grid_block_number_y = Math.floor(grid_height / grid_block_size);
84 | grid = [];
85 | for (var x = 0; x <= grid_block_number_x; x++) {
86 | grid[x] = [];
87 | for (var y = 0; y <= grid_block_number_y; y++) {
88 | grid[x][y] = null;
89 | }
90 | }
91 | markers = [];
92 |
93 | // Create button for each tool
94 | $("#tools").html(Object.entries(tools).map(([tool_name, props]) =>
95 | ``
98 | ));
99 | draw();
100 | }
101 |
102 | function resize_grid() {
103 | $('#container').css('width', grid_width);
104 | $('#container').css('height', grid_height);
105 | $('#canvas').attr('width', $('#container').css('width'));
106 | $('#canvas').attr('height', $('#container').css('height'));
107 | grid_block_size = $('#gridblocksize').val() * 1;
108 | grid_block_number_x = Math.floor(grid_width / grid_block_size);
109 | grid_block_number_y = Math.floor(grid_height / grid_block_size);
110 | var new_grid = [];
111 | for (var x = 0; x <= grid_block_number_x; x++) {
112 | x_prev = Math.floor(x * grid_block_size / prev_grid_block_size);
113 | new_grid[x] = [];
114 | for (var y = 0; y <= grid_block_number_y; y++) {
115 | y_prev = Math.floor(y * grid_block_size / prev_grid_block_size);
116 | new_grid[x][y] = grid[x_prev][y_prev];
117 | }
118 | }
119 | prev_grid_block_size = grid_block_size;
120 | grid = new_grid;
121 | draw();
122 | }
123 |
124 | function draw() {
125 | drawBoard();
126 | drawGrid();
127 | drawMarkers();
128 | }
129 |
130 | function drawBoard() {
131 | context.strokeStyle = 'rgba(0,0,0,0.3)';
132 | for (var x = 0; x <= grid_width; x += grid_block_size) {
133 | context.moveTo(x, 0);
134 | context.lineTo(x, grid_width);
135 | }
136 | for (var y = 0; y <= grid_width; y += grid_block_size) {
137 | context.moveTo(0, y);
138 | context.lineTo(grid_width, y);
139 | }
140 | context.stroke();
141 | }
142 |
143 | function changeOpacity() {
144 | tools[tools_keys[erase_key]].color = tools[tools_keys[erase_key]].color.replace(current_opacity, $('#eraseropacity').val() * 1);
145 | current_opacity = $('#eraseropacity').val() * 1;
146 | draw();
147 | }
148 |
149 | function getMousePos(e) {
150 | var rect = canvas.getBoundingClientRect();
151 | return {
152 | x: e.clientX - rect.left,
153 | y: e.clientY - rect.top
154 | };
155 | }
156 |
157 | function toggleGrid(e) {
158 | if (e.type === 'mousemove' && e.buttons !== 1) {
159 | return;
160 | }
161 | var pos = getMousePos(e);
162 |
163 | var x = ((pos.x + (grid_block_size - pos.x % grid_block_size)) / grid_block_size) - 1;
164 | var y = ((pos.y + (grid_block_size - pos.y % grid_block_size)) / grid_block_size) - 1;
165 |
166 | if (grid[x][y] != null && selected_tool == erase_key) {
167 | grid[x][y] = null;
168 | } else {
169 | grid[x][y] = selected_tool;
170 | }
171 | }
172 |
173 | function drawGrid() {
174 | context.drawImage(background_img, 0, 0, background_img.width, background_img.height);
175 | for (var x = 0; x <= grid_block_number_x; x++) {
176 | for (var y = 0; y <= grid_block_number_y; y++) {
177 | if (grid[x][y] != null) {
178 | context.fillStyle = tools[grid[x][y]].color;
179 | context.fillRect(x * grid_block_size + 1, y * grid_block_size + 1, grid_block_size - 1, grid_block_size - 1);
180 | } else {
181 | context.fillStyle = tools[tools_keys[erase_key]].color;
182 | context.fillRect(x * grid_block_size + 1, y * grid_block_size + 1, grid_block_size - 1, grid_block_size - 1);
183 | }
184 | }
185 | }
186 | context.fill();
187 | }
188 |
189 | function drawMarkers() {
190 | for (var i = 0; i < markers.length; i++) {
191 | //context.moveTo(markers[i][0], markers[i][1]);
192 | context.beginPath();
193 | // change opacity to full
194 | context.fillStyle = tools[markers[i][2]].color.replace(/,(\d+\.?\d*)\)/g, ',1)');
195 | context.arc(markers[i][0], markers[i][1], 3, 0, 2 * Math.PI);
196 | context.closePath();
197 | context.fill();
198 | context.beginPath();
199 | context.strokeStyle = 'white';
200 | context.arc(markers[i][0], markers[i][1], 4, 0, 2 * Math.PI);
201 | context.closePath();
202 | context.stroke();
203 | }
204 | }
205 |
206 | function changeImage() {
207 | $('#modal').fadeIn(400, function() {
208 | img = $('#gridimage')[0].files[0];
209 | if (img.type.match('image.*')) {
210 | reader = new FileReader();
211 | reader.readAsDataURL(img);
212 | reader.onload = function(e) {
213 | if (e.target.readyState == FileReader.DONE) {
214 | background_img = new Image();
215 | background_img.src = e.target.result;
216 | background_img.onload = function() {
217 | $('#imagedetails').text('Size: ' + background_img.width + ' x ' + background_img.height + ' px');
218 | if (background_img.width > parseInt($('#container').css('max-width'), 10) || background_img.height > parseInt($('#container').css('max-height'), 10)) {
219 | var larger_dimension = 0;
220 | if (background_img.width > background_img.height) {
221 | larger_dimension = background_img.width;
222 | } else {
223 | larger_dimension = background_img.height;
224 | }
225 | ratio = parseInt($('#container').css('max-width'), 10) / larger_dimension;
226 | background_img.width = Math.floor(background_img.width * ratio);
227 | background_img.height = Math.floor(background_img.height * ratio);
228 | }
229 | grid_width = background_img.width;
230 | grid_height = background_img.height;
231 | context.beginPath();
232 | context.clearRect(0, 0, grid_width, grid_height);
233 | setup();
234 | }
235 | background_img.onerror = function() {
236 | window.alert('Error loading image. Please try again.');
237 | }
238 | }
239 | }
240 | } else {
241 | window.alert('Not an image. Please select an image file - JPG, PNG etc.');
242 | }
243 | $('#modal').fadeOut();
244 | });
245 | }
246 |
247 | $('#canvas').bind('mousewheel DOMMouseScroll', function(e) {
248 | if (e.originalEvent.wheelDelta > 0 || e.originalEvent.detail < 0) {
249 | new_tool = tools_keys.indexOf(selected_tool) + 1;
250 | } else {
251 | new_tool = tools_keys.indexOf(selected_tool) - 1;
252 | }
253 | changeTool(new_tool);
254 | });
255 |
256 | function changeTool(index) {
257 | const new_tool = Math.max(0, Math.min(index, tools_length - 1));
258 |
259 | selected_tool = tools_keys[new_tool];
260 | $('.tool-button').each((i, button) => {
261 | const tool = $(button).data().tool;
262 | $(button).toggleClass('pure-button-active', tool === selected_tool)
263 | })
264 | }
265 |
266 | function saveImage() {
267 | makeVirtual();
268 | var imageurl = virtual_canvas.toDataURL('image/jpg', 0.85);
269 | $('#save').attr('href', imageurl); // it will save locally
270 | $('#virtual').hide();
271 | return false;
272 | }
273 |
274 | function saveGrid() {
275 | copy = background_img;
276 | background_img = new Image();
277 | context.beginPath();
278 | context.clearRect(0, 0, grid_width, grid_height);
279 | drawBoard();
280 | drawGrid();
281 | drawMarkers();
282 | makeVirtual();
283 | var imageurl = virtual_canvas.toDataURL('image/png');
284 | $('#savegrid').attr('href', imageurl);
285 | $('#virtual').hide();
286 | background_img = copy;
287 | return false;
288 | }
289 |
290 | function makeVirtual() {
291 | $('#virtual').attr('width', parseInt($('#canvas').attr('width'), 10));
292 | $('#virtual').attr('height', parseInt($('#canvas').attr('height'), 10) + 100);
293 | virtual_context.fillStyle = 'white';
294 | virtual_context.fillRect(0, 0, parseInt($('#virtual').attr('width'), 10), parseInt($('#virtual').attr('height'), 10));
295 | virtual_context.drawImage(canvas, 0, 0);
296 | var x = 20;
297 | var y = parseInt($('#virtual').attr('height'), 10) - 70;
298 | virtual_context.font = "16px Arial";
299 | var counts = {};
300 | var count_total = 0;
301 | for (var temp_x = 0; temp_x <= grid_block_number_x; temp_x++) {
302 | for (var temp_y = 0; temp_y <= grid_block_number_y; temp_y++) {
303 | if (grid[temp_x][temp_y] != null) {
304 | if (!counts[grid[temp_x][temp_y]]) {
305 | counts[grid[temp_x][temp_y]] = 0;
306 | }
307 | counts[grid[temp_x][temp_y]]++;
308 | count_total++;
309 | }
310 | }
311 | }
312 | var marker_counts = {};
313 | for (var i = 0; i < markers.length; i++) {
314 | if (marker_counts[markers[i][2]] == undefined) {
315 | marker_counts[markers[i][2]] = 0;
316 | }
317 | marker_counts[markers[i][2]]++;
318 | }
319 | var percentages = {};
320 | for (const [key, value] of Object.entries(counts)) {
321 | percentages[key] = (value / count_total) * 100;
322 | }
323 | var percentages_rounded = largestRemainderRound(percentages, 100);
324 | // ugly iterator, feel free to fix based on adapted largest remainder rounding function
325 | var i = 0;
326 | for (const [key, value] of Object.entries(percentages)) {
327 | percentages[key] = percentages_rounded[i];
328 | i++;
329 | }
330 | for (i = 0; i < tools_length - 1; i++) { // do not include last tool - the eraser
331 | if (percentages[tools_keys[i]]) {
332 | var percentage_string = ' (' + percentages[tools_keys[i]] + '%)';
333 | if (tools[tools_keys[i]].markers && marker_counts[tools_keys[i]]) {
334 | percentage_string = percentage_string + ', ' + marker_counts[tools_keys[i]] + ' counted';
335 | }
336 | var percentage_string = percentage_string + ' ';
337 | var text_width = virtual_context.measureText(tools[tools_keys[i]].desc + percentage_string).width;
338 | if (x + text_width > parseInt($('#canvas').attr('width'), 10)) {
339 | y = y + 21;
340 | x = 18;
341 | }
342 | virtual_context.fillStyle = tools[tools_keys[i]].color.replace(current_opacity, '1');
343 | virtual_context.fillText(tools[tools_keys[i]].desc[0], x, y);
344 | virtual_context.fillStyle = 'black';
345 | var first_char_width = virtual_context.measureText(tools[tools_keys[i]].desc[0]).width;
346 | virtual_context.fillText(tools[tools_keys[i]].desc.substr(1) + percentage_string, x + first_char_width, y);
347 | x = x + text_width + 18;
348 | }
349 | }
350 | branding = 'The Arrogance of Space Mapping Tool';
351 | branding_x = parseInt($('#virtual').attr('width'), 10) - 10 - virtual_context.measureText(branding).width;
352 | branding_y = parseInt($('#virtual').attr('height'), 10) - 12;
353 | virtual_context.beginPath();
354 | virtual_context.fillStyle = 'black';
355 | virtual_context.fillRect(branding_x - 12, branding_y - 18, virtual_context.measureText(branding).width - 13, 27);
356 | virtual_context.closePath();
357 | virtual_context.font = "bold 13px Arial";
358 | virtual_context.fillStyle = 'white';
359 | virtual_context.fillText(branding, branding_x, branding_y);
360 | }
361 |
362 | function reset() {
363 | var answer = confirm("Reset will erase any changes. Continue?");
364 | if (answer) {
365 | window.location.reload(true);
366 | }
367 | }
368 |
369 | function createMarker(e) {
370 | if (tools[selected_tool].markers) {
371 | var pos = getMousePos(e);
372 | markers.push([pos.x, pos.y, selected_tool]);
373 | }
374 | }
375 |
376 | $('input').keydown(function(e) {
377 | if (e.keyCode == 13) {
378 | e.preventDefault();
379 | return false;
380 | }
381 | });
382 | $('form').submit(function(e) {
383 | e.preventDefault();
384 | return false;
385 | });
386 | $('#gridblocksize').val(grid_block_size);
387 | $('#gridimage').change(function() {
388 | changeImage();
389 | });
390 | $('#gridblocksize').change(function(e) {
391 | e.preventDefault();
392 | resize_grid();
393 | drawBoard();
394 | return false;
395 | });
396 | $('#eraseropacity').on('input change', function() {
397 | changeOpacity();
398 | draw();
399 | });
400 | $('#canvas').bind('click mousemove', function(e) {
401 | toggleGrid(e);
402 | drawGrid();
403 | drawMarkers();
404 | });
405 | $(document).keyup(function(e) {
406 | if (e.key == 'Backspace') {
407 | markers.pop();
408 | }
409 | drawGrid();
410 | drawMarkers();
411 | });
412 | $('#canvas').contextmenu(function(e) {
413 | createMarker(e);
414 | drawMarkers();
415 | return false;
416 | });
417 | $(document).on('click', '.tool-button', function() {
418 | const tool_name = $(this).data().tool;
419 | const tool_index = tools_keys.indexOf(tool_name);
420 |
421 | changeTool(tool_index)
422 | });
423 | $('#save').click(function() {
424 | saveImage();
425 | });
426 | $('#savegrid').click(function() {
427 | saveGrid();
428 | });
429 | $('#reset').click(function() {
430 | reset();
431 | });
432 |
433 | /**
434 | * largestRemainderRound will round each number in an array to the nearest
435 | * integer but make sure that the the sum of all the numbers still equals
436 | * desiredTotal. Uses Largest Remainder Method. Returns numbers in order they
437 | * came.
438 | *
439 | * @param {number[]} numbers - numbers to round
440 | * @param {number} desiredTotal - total that sum of the return list must equal
441 | * @return {number[]} the list of rounded numbers
442 | * @example
443 | *
444 | * var numbers = [13.6263, 47.9896, 9.59600 28.7880]
445 | * largestRemainderRound(numbers, 100)
446 | *
447 | * // => [14, 48, 9, 29]
448 | *
449 | * adapted from: https://gist.github.com/scwood/e58380174bd5a94174c9f08ac921994f
450 | */
451 | function largestRemainderRound(numbers, desiredTotal) {
452 | if (Object.keys(numbers).length === 0 && numbers.constructor === Object) {
453 | return 0;
454 | }
455 | numbers = Object.keys(numbers).map((key) => [numbers[key]]);
456 | var result = numbers.map(function(number, index) {
457 | return {
458 | floor: Math.floor(number),
459 | remainder: getRemainder(number),
460 | index: index,
461 | };
462 | }).sort(function(a, b) {
463 | return b.remainder - a.remainder;
464 | });
465 |
466 | var lowerSum = result.reduce(function(sum, current) {
467 | return sum + current.floor;
468 | }, 0);
469 |
470 | var delta = desiredTotal - lowerSum;
471 | for (var i = 0; i < delta; i++) {
472 | result[i].floor++;
473 | }
474 |
475 | return result.sort(function(a, b) {
476 | return a.index - b.index;
477 | }).map(function(result) {
478 | return result.floor;
479 | });
480 | }
481 |
482 | function getRemainder(number) {
483 | var remainder = number - Math.floor(number);
484 | return remainder.toFixed(4);
485 | }
486 |
487 | initialize();
--------------------------------------------------------------------------------