21 | {% endblock %}
22 |
23 | {% block extrabody %}
24 |
25 |
26 |
27 |
28 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2007-2014 Open Knowledge Foundation
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/frontend/index.html:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
68 | {{ gettext('2. Connect and Customize') }}
69 |
70 | {% if not currentUser %}
71 |
72 | {{ gettext("ALERT: you are not signed in so your timemap will be created 'anonymously'.") }}
73 |
74 |
75 | {{ gettext('If want to \'own\' your timemap you should sign in (or sign-up) now »') }}
76 |
77 | {{ gettext('(Sign-up takes a few seconds with your twitter account »)') }}
78 |
79 |
80 | {{ gettext('Find out more on anonymous vs logged in') }} - {{ gettext('read FAQ below') }} »
81 |
82 | {% endif %}
83 |
84 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
{{ gettext('FAQ') }}
106 |
{{ gettext('Can I make a timemap anonymously?') }}
107 |
{{ gettext('Yes! You do not need an account to create a timemap - they can be created anonymously and will have all the same features and shareability of normal timemaps. However, there are some benefits of creating an account and creating your timemap whilst logged in:') }}
108 |
109 |
{{ gettext("You'll get a nice URL for your timemap at /your-username/a-name-you-choose-for-your-timemap") }}
110 |
{{ gettext('All of your timemaps will be nicely listed at /your-username') }}
111 |
{{ gettext("As you'll be identified as the owner you'll be able to re-configure (or delete) your timemap later") }}
{{ gettext("Go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. This tutorial walks you through.") }}
118 |
119 |
120 |
121 |
{{ gettext("What URL do I use to connect my spreadsheet?") }}
122 |
{{ gettext("Use the URL you get by clicking your spreadsheet's Share button and copying the Link to share box.") }}
123 |
124 |
{{ gettext("Note that although you must also Publish to the web, TimeMapper does not use the URL found in the publication pop-up.") }}
125 |
126 |
{{ gettext("What structure must the spreadsheet have?") }}
127 |
{{ gettext('TimeMapper recognizes certain columns with specific names. The best overview of these columns is the template, which has rows of instructions and examples.') }}
128 |
129 |
{{ gettext('Not all fields are required. The only required fields are Title and Start fields, and even Start can be omitted if you just want a map. Note that you can add any number of other columns beyond those that TimeMapper uses.') }}
130 |
131 |
{{ gettext('How do I format dates?') }}
132 |
{{ gettext('The preferred date format is ISO 8601 (YYYY-MM-DD), but TimeMapper recognizes most types of date.') }}
133 |
134 |
{{ gettext("If a date's month and day are ambiguous (e.g. is 08-03-1798 UK notation for 8 March, or is it US notation for 3 August?), by default, the first number will be interpreted as the month. You can change this by clicking the edit button in the top right corner of your TimeMap's display and selecting between US- and non-US-style dates.") }}
135 |
136 |
{{ gettext('What kinds of geodata are supported?') }}
137 |
{{ gettext('The Location column accepts two types of geodata: latitude-longitude coordinates or GeoJSON objects.') }}
138 |
139 |
{{ gettext('Coordinates must be in the format lat, long (e.g. 37.5, -122). The spreadsheet template includes a formula which automatically looks up coordinates corresponding to human-readable place names in the Place column. This formula is explained in a School of Data blog post.') }}
140 |
141 |
{{ gettext('Advanced users who want to go beyond simple coordinates can use GeoJSON feature objects. For an example, see this blog post on adding GeoJSON country boundaries to spreadsheets.') }}
142 |
143 |
144 |
145 | {% endblock %}
146 |
147 | {% block extrabody %}
148 |
149 |
150 |
151 |
152 |
153 |
154 | {% endblock %}
155 |
--------------------------------------------------------------------------------
/public/css/style.css:
--------------------------------------------------------------------------------
1 | @import url(https://fonts.googleapis.com/css?family=News+Cycle:400,700|Merriweather:400,300,700);
2 |
3 | html {
4 | height: 100%;
5 | }
6 |
7 | body {
8 | font-size: 14px;
9 | font-family: Merriweather, serif;
10 | font-weight: 300;
11 | }
12 |
13 | h1, h2, h3, h4, h5, h6, .brand {
14 | font-family: "News Cycle", Arial, sans-serif;
15 | font-weight: 700;
16 | }
17 |
18 | h3 {
19 | line-height: 32px;
20 | }
21 |
22 | input.input {
23 | box-sizing: border-box;
24 | height: 30px;
25 | width: 100%;
26 | }
27 |
28 | .page-header {
29 | border-bottom: 1px solid #ccc;
30 | padding-bottom: 5px;
31 | }
32 |
33 | .footer {
34 | font-size: 90%;
35 | background-color: #f5f5f5;
36 | padding: 20px 0px;
37 | margin-top: 20px;
38 | border-top: solid 1px #ddd;
39 | }
40 |
41 | i.gdrive {
42 | height: 20px;
43 | width: 20px;
44 | display: inline-block;
45 | vertical-align: top;
46 | background: url(drive20.png);
47 | }
48 |
49 | /* we do not have a LH sidebar */
50 | .container-fluid > .content {
51 | margin-left: 0;
52 | }
53 |
54 | .okfn-ribbon {
55 | position: absolute;
56 | top: -10px;
57 | right: 10px;
58 | z-index: 10000;
59 | }
60 |
61 | #okf-panel {
62 | z-index: 10000;
63 | top: 0;
64 | position: absolute;
65 | width: 100%;
66 | overflow: hidden;
67 | }
68 |
69 | /* Custom scroll-bars */
70 | ::-webkit-scrollbar {
71 | width: 10px;
72 | height: 10px;
73 | }
74 | ::-webkit-scrollbar-track {
75 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
76 | }
77 | ::-webkit-scrollbar-thumb {
78 | background: rgba(150,150,150,0.8);
79 | }
80 |
81 | /**********************************************************
82 | * Navigation
83 | *********************************************************/
84 |
85 | .navbar {
86 | margin-bottom: 5px;
87 | }
88 |
89 | .navbar-inner {
90 | background: #e0e7eb;
91 | padding-top: 0px;
92 | }
93 |
94 | .navbar .brand {
95 | color: #434C50;
96 | font-weight: 200;
97 | padding-top: 7px;
98 | padding-bottom: 6px;
99 | }
100 |
101 | .black {
102 | color: #333;
103 | }
104 |
105 | .brand-ext {
106 | color: #08C;
107 | }
108 |
109 | .navbar .brand.by {
110 | padding-left: 6px;
111 | }
112 |
113 | .brand.by span {
114 | font-size: 75%;
115 | margin-right: 2px;
116 | }
117 |
118 | .brand img {
119 | margin-top: -3px;
120 | height: 30px;
121 | margin-right: 3px;
122 | }
123 |
124 | .navbar .divider-vertical {
125 | background-color: #acbfca;
126 | border-right: 1px solid #f5f7f8;
127 | width: 1px;
128 | margin: 0;
129 | }
130 |
131 | .navbar .nav > li > a {
132 | color: #434c50;
133 | text-shadow: none;
134 | }
135 |
136 | .navbar .nav > li > a:hover {
137 | color: #222;
138 | }
139 |
140 | .navbar ul.nav.pull-right {
141 | margin-right: 35px;
142 | }
143 |
144 | /**************************************************************
145 | * Front Page
146 | *************************************************************/
147 |
148 | .home .hero-unit {
149 | margin-top: 45px;
150 | }
151 |
152 | .home .examples img {
153 | width: 100%;
154 | height: 210px;
155 | }
156 |
157 | /**************************************************************
158 | * Data Views
159 | *************************************************************/
160 |
161 | body.view {
162 | height: 100%;
163 | }
164 |
165 | body.view .navbar {
166 | margin-bottom: 0px;
167 | }
168 |
169 | body.view .navbar ul.pull-right a {
170 | padding: 10px 10px 10px;
171 | }
172 |
173 | body.view li.tweet {
174 | margin-top: 10px;
175 | }
176 |
177 | body.view .content {
178 | background-color: none;
179 | padding: 0px;
180 | margin: 0;
181 | -webkit-box-shadow: none;
182 | -moz-box-shadow: none;
183 | box-shadow: none;
184 | }
185 |
186 | body.view .controls {
187 | display: none;
188 | position: absolute;
189 | left: 0;
190 | z-index: 1000;
191 | padding-top: 2px;
192 | }
193 |
194 | body.view .controls .toolbox {
195 | margin-left: -2px;
196 | }
197 |
198 | body.view .controls .toolbox.hideme input {
199 | display: none;
200 | }
201 |
202 | body.view .footer {
203 | position: absolute;
204 | bottom: 0;
205 | left: 0;
206 | right: 0;
207 | margin: 0;
208 | padding: 8px;
209 | }
210 |
211 | body.view .loading {
212 | position: absolute;
213 | top: 50%;
214 | left: 0;
215 | width: 100%;
216 | text-align: center;
217 | font-size: 45px;
218 | }
219 |
220 | body.view .navbar ul.nav.pull-right {
221 | margin-right: 0;
222 | }
223 |
224 | .data-views {
225 | position: absolute;
226 | top: 42px;
227 | bottom: 41px;
228 | left: 0;
229 | right: 0;
230 | }
231 |
232 | .panes, .panes > div, .map, .map > div, .recline-map, .recline-map .map, .recline-timeline {
233 | height: 100%;
234 | }
235 |
236 | .data-views .recline-map {
237 | margin-left: 0px;
238 | }
239 |
240 | /* correct so we have full width of page */
241 | .data-views .timeline-pane {
242 | margin: 0;
243 | padding: 0;
244 | width: 66.66%;
245 | float: left;
246 | }
247 | .data-views .map-pane {
248 | margin: 0;
249 | padding: 0;
250 | width: 33.33%;
251 | float: left;
252 | }
253 |
254 | .timeline {
255 | position: relative;
256 | height: 100%;
257 | }
258 |
259 | .vco-storyjs p {
260 | font-size: 13px;
261 | }
262 |
263 | /* get attribution on one line ... */
264 | .leaflet-container .leaflet-control-attribution {
265 | font-size: 9px;
266 | }
267 |
268 | .leaflet-control-attribution img {
269 | display: none;
270 | }
271 |
272 | /**************************************************************
273 | * Embed
274 | *************************************************************/
275 |
276 | body.embed .navbar {
277 | display: none;
278 | }
279 |
280 | body.embed .data-views {
281 | top: 0;
282 | }
283 |
284 | /**************************************************************
285 | * Timeline tweaks
286 | *************************************************************/
287 |
288 | .source {
289 | font-style: italic;
290 | }
291 |
292 | .vco-storyjs a.title-link {
293 | color: #0088cc;
294 | }
295 |
296 | i.title-link {
297 | font-size: 50%;
298 | color: #000;
299 | }
300 |
301 | /* hack to hide the link icon in the forward / back links at the side */
302 | .nav-container i.title-link {
303 | display: none;
304 | }
305 |
306 | /**********************************************************
307 | * Timeline Only View
308 | *********************************************************/
309 |
310 | body.viewtype-timeline .map-pane {
311 | display: none;
312 | }
313 |
314 | body.viewtype-timeline .timeline-pane {
315 | width: 100%;
316 | }
317 |
318 | /**********************************************************
319 | * Timeline Only View
320 | *********************************************************/
321 |
322 | body.viewtype-map .timeline-pane {
323 | display: none;
324 | }
325 |
326 | body.viewtype-map .map-pane {
327 | width: 100%;
328 | height: 100%;
329 | }
330 |
331 | /**********************************************************
332 | * Dashboard View
333 | *********************************************************/
334 |
335 | .dashboard .menu {
336 | margin: 50px 0px;
337 | }
338 |
339 | /**********************************************************
340 | * Create View
341 | *********************************************************/
342 |
343 | .create .overview h3 {
344 | margin-top: 0;
345 | padding-top: 0px;
346 | }
347 |
348 | .help-block {
349 | font-size: 90%;
350 | }
351 |
352 | body.create h2, body.create .instructions h3 {
353 | margin-top: 0;
354 | margin-bottom: 20px;
355 | text-align: center;
356 | }
357 |
358 | /**********************************************************
359 | * User View
360 | *********************************************************/
361 |
362 | .gravatar {
363 | border-radius: 3px;
364 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
365 | }
366 |
367 | body.account .page-header .gravatar {
368 | margin-top: -9px;
369 | }
370 |
371 | body.account .meta {
372 | font-size: 16px;
373 | }
374 |
375 | body.account .meta ul {
376 | list-style-type: none;
377 | margin-left: 0;
378 | }
379 |
380 | body.account .meta ul.vcard {
381 | margin-top: 8px;
382 | }
383 |
384 | body.account .meta ul.vcard li {
385 | margin-bottom: 5px;
386 | }
387 |
388 | body.account .meta ul.vcard .description {
389 | font-size: 13px;
390 | margin-top: 8px;
391 | }
392 |
393 | .meta ul.stats li {
394 | display: inline-block;
395 | }
396 |
397 | .meta ul.stats li strong {
398 | display: block;
399 | font-size: 42px;
400 | line-height: 50px;
401 | }
402 |
403 | /**********************************************************
404 | * Views - List
405 | *********************************************************/
406 |
407 | ul.view-summary-list {
408 | padding-left: 0;
409 | list-style-type: none;
410 | margin-left: 0;
411 | margin-bottom: 50px;
412 | }
413 |
414 | ul.view-summary-list li {
415 | border-bottom: solid 1px #ccc;
416 | padding-bottom: 10px;
417 | margin-bottom: 10px;
418 | }
419 |
420 | .view-summary-list h3:first-child {
421 | margin-top: 0;
422 | }
423 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TimeMapper
2 |
3 | Create beautiful timelines and timemaps from Google Spreadsheets.
4 |
5 | Built by members of [Open Knowledge Foundation Labs](http://okfnlabs.org/).
6 |
7 | See it in action at
8 |
9 | # Install
10 |
11 | ## Local Install
12 |
13 | This is a Node web-app built using express.
14 |
15 | Install Node (>=0.8 suggested) and npm then checkout the code:
16 |
17 | git clone https://github.com/okfn/timemapper
18 |
19 | Then install the dependencies:
20 |
21 | cd timemapper
22 | npm install .
23 | # for some vendor modules
24 | git submodule init && git submodule update
25 |
26 | Finally, you may wish to set configuration options such as database name, port
27 | to run on, etc. To do this:
28 |
29 | # copy the settings.json template to settings.json
30 | cp settings.json.tmpl settings.json
31 | # then edit as necessary
32 |
33 | Now you can run the app:
34 |
35 | node run.js
36 |
37 | To view the site, open localhost:3000 in a browser.
38 |
39 | ### Configuration
40 |
41 | The default configuration can be found in `lib/config.js`. You can override
42 | this in a couple of ways:
43 |
44 | 1. Create a `settings.json` with specific values. `settings.json` has the same
45 | form as nconf.defaults object in in `lib/config.js`.
46 |
47 | 2. Set specific environment variables. The ones you can set are those used in
48 | `nconf.defaults`. This useful for deployment on Heroku where environment
49 | variables are default way to configure.
50 |
51 | ## Deploy (to Heroku)
52 |
53 | Standard stuff:
54 |
55 | heroku create timemapper
56 | git push heroku
57 |
58 | You'll also need to set config. Suggest creating a `.env` file:
59 |
60 | TWITTER_KEY=...
61 | ...
62 |
63 | Then push it:
64 |
65 | heroku config:push
66 |
67 |
68 |
69 | # Overview for Developers
70 |
71 | * NodeJS app but very frontend JS oriented
72 | * Most of "presentation" including visualizations are almost entirely in frontend javascript
73 | * Backend storage of "metadata" is onto s3 or local filesystem with storage of
74 | actual data (data for timelines/timemaps etc) into google docs spreadsheets
75 |
76 | ## Backend Storage
77 |
78 | Layout follows frontend urls:
79 |
80 | /{username}/data.json # user info
81 | /{username}/{dataview}/datapackage.json # config for the dataview
82 | /{username}/{dataview}/... other files # (none atm but possibly we store data locally in future)
83 |
84 | ## Data View info
85 |
86 | Stored in datapackage.json following [Data Package spec][dp-spec]. Key points:
87 |
88 | * name, title, licenses etc as per [Data Package][dp-spec]
89 | * info on google doc data source stored in first resources item in format compatible with [Recline][]:
90 |
91 | resources: [{
92 | backend: 'gdocs',
93 | url: 'gdocs url ...'
94 | }]
95 |
96 | * additional config specific to timemapper in item call tmconfig. We will
97 | be gradually adding values here but at the moment have:
98 |
99 | tmconfig: {
100 | dayfirst: false # are dates dayfirst
101 | startfrom: start # start | end | today
102 | layout: timemap # timemap | map | timeline
103 | timelineJSOptions: # options to pass to timelinejs
104 | }
105 |
106 | [dp-spec]: http://data.okfn.org/standards/data-package
107 | [Recline]: http://okfnlabs.org/recline/
108 |
109 | ## Translation info
110 |
111 | TimeMappers uses `i18n-abide` with `gettext`.
112 |
113 | It currently supporte loclaes `en-US`, `zh-TW` locales.
114 |
115 | To update po files after modify `views/*.html`
116 |
117 | ```
118 | $ npm run update-po
119 | ```
120 |
121 | To re-compile translation json files.
122 |
123 | ```
124 | $ npm run gen-po-json
125 | ```
126 |
127 | for more details, please read [Mozilla - Localization in Action][localization-in-action].
128 |
129 | [localization-in-action]: https://hacks.mozilla.org/2013/04/localization-in-action-part-3-of-3-a-node-js-holiday-season-part-11/
130 |
131 | # User Stories
132 |
133 | Alice: user, who wants to create timelines, timemaps etc
134 | Bob: visitor (and potential user)
135 | Charlie: Admin of the website
136 |
137 |
138 | ## Register / Login
139 |
140 | [ip] As Alice I want to signup (using Twitter?) so that I have an account and can login
141 |
142 | As Alice I want to login (using Twitter?) so that I am identified to the system and the Vizs I create are owned by me
143 |
144 | As Alice I want to see a terms of service when I signup so that I know what the licensing arrangements are for what I create and do
145 |
146 | ## Create and Edit Views
147 |
148 | As Alice I want to create a timemap Viz quickly from a google spreadsheet ...
149 | - I want to set the title and "slug" for my timemap and have a nice url /alice/{name-of-viz}
150 | - I want to choose a license (or full copyright) - default license applied. Licenses will be open licenses or full copyright.
151 | - I want to create an animated timemap in which the time and map interact ...
152 | - I want to add a description (and attribution) to my Viz (perhaps now or later ...)
153 |
154 | As Alice I want to edit my Viz later after I've created it (e.g. change the title) so that I can correct typos or update info to reflect changes
155 |
156 | As Alice I want to create a timeline Viz quickly from my google spreadsheet so that I can share it with others
157 |
158 | As Alice I want to create a timemap / timeline quickly from a gist so that I can share it with others
159 | - Structure of gist??
160 |
161 | As Alice I want to create a map quickly from a google spreadsheet ...
162 |
163 | As Alice or Bob I want to embed my Viz in a website elsewhere so that people can see it there
164 |
165 | As Alice I want to watch a short (video) tutorial introducing me to how this works so that I have help getting started
166 |
167 | As Alice or Bob I want to create a Viz without logging in so that I can try out the system without signing up
168 | - Is this necessary if sign up is really easy?
169 |
170 | ## Forking
171 |
172 | I want to "fork" someone elses visualization so that I can modify and extend it
173 |
174 | ## Listing and Admin
175 |
176 | [x] As Alice I want to list the "Vizs (viz?)" I've created
177 |
178 | [ip] As Bob I want to know what I can do with this service before I sign up so that I know whether it is worth doing so
179 | - Some featured timemaps ...
180 |
181 | [x] As Bob I want to see all the Vizs created by Alice so that I can see if there some I like
182 |
183 | - Most recent items ?
184 |
185 | As Bob I want to see recent activity by Alice to get a sense of the cool stuff she has been doing so that I know to look at that stuff first
186 |
187 | As Alice I want to delete a Viz so that it is not available anymore (because I don't want it visible)
188 |
189 | As Alice I want to undo deletiion of a Viz that I accidentally deleted so that it is available again
190 |
191 | As Alice I want to revert to previous versions of my Viz so that I can see what it was like before
192 |
193 | As Alice I want to "hide" a Viz so that it is not visible to others (but is visible to me)
194 | - Is this hidden in the listing or more than that? What about people who already have the url
195 |
196 | As Charlie I want to be able to delete someone's Viz (or account) so that it no longer is available (because they want it down or someone else does etc)
197 |
198 | ## Access Control
199 |
200 | As Alice I want to allow a Viz built on a private spreadsheet in google docs so that I don't have to make that spreadsheet public to create a Viz of it
201 |
202 | As Alice I want to restrict access to some of my Vizs so that only I can see them
203 |
204 | As Alice I want to restrict access to some of my Vizs but allow specific other people to view it so that other people than me can see it
205 |
206 | ## Misc
207 |
208 | As Bob I want to find out about the website and project so that I get a sense of who's behind it / whether its trustworthy / whether there is other cool stuff they do
209 |
210 | As Alice I want to know how many people have viewed my Viz so that I know whether other people are interested
211 |
212 | As Bob (? may need to be logged in) I want to "star" a Viz I come across so that I recognize its value and store it for finding later
213 |
214 | ## Asides
215 |
216 | - config
217 | - layout of the map / timeline (stacked versus side by side)
218 | - date parsing
219 | - 2 types of data source - gists as well as google docs
220 | - data package structure in gists!
221 |
222 | # License, Contributors and History
223 |
224 | This is an open-source project licensed under the MIT License - see `LICENSE` file for details.
225 |
226 | Contributors include:
227 |
228 | * Rufus Pollock (Open Knowledge)
229 | * Dan Wilson -
230 | * Chen Hsin-Yi -
231 |
232 | We also use a whole bunch of fantastic open-source libraries, including:
233 |
234 | * TimelineJS
235 | * ReclineJS
236 | * Leaflet
237 | * Backbone
238 | * Bootstrap
239 |
240 | First version was Microfacts / Weaving History
241 | which ran from 2007-2010.
242 |
243 |
--------------------------------------------------------------------------------
/test/app.test.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | path = require('path')
3 | , request = require('supertest')
4 | , express = require('express')
5 | , assert = require('assert')
6 | , dao = require('../lib/dao.js')
7 | , logic = require('../lib/logic.js')
8 | , config = require('../lib/config.js')
9 | , _ = require('underscore')
10 | , base = require('./base');
11 | ;
12 |
13 | // make sure we are in testing mode
14 | config.set('test:testing', true);
15 | config.set('twitter:key', 'test');
16 | config.set('twitter:secret', 'test');
17 | config.set('database:backend', 'fs');
18 |
19 | var app = require('../app.js').app;
20 |
21 | describe('API', function() {
22 | before(function(done) {
23 | base.resetDb();
24 | done();
25 | });
26 |
27 | it('Account GET', function(done) {
28 | request(app)
29 | .get('/api/account/' + 'tester')
30 | .end(function(err, res) {
31 | // console.log(res);
32 | assert.equal(res.body.email, undefined, 'Email should not be in Account object');
33 | done();
34 | });
35 | });
36 | it('DataView GET', function(done) {
37 | request(app)
38 | .get('/api/dataview/tester/napoleon')
39 | .expect('Content-Type', /json/)
40 | .end(function(err, res) {
41 | // console.log(res);
42 | assert.equal(res.body.name, 'napoleon');
43 | assert.equal(res.body.title, 'Battles in the Napoleonic Wars');
44 | done();
45 | });
46 | });
47 | it('DataView Create Conflict if object with name already exists', function(done) {
48 | request(app)
49 | .post('/api/dataview/')
50 | .send({
51 | owner: 'tester',
52 | name: 'napoleon'
53 | })
54 | .expect('Content-Type', /json/)
55 | .expect(409, done)
56 | ;
57 | });
58 | it('DataView not Authz', function(done) {
59 | var data = {
60 | owner: 'not-tester',
61 | name: 'test-api-create'
62 | };
63 | request(app)
64 | .post('/api/dataview/')
65 | .send(data)
66 | .expect('Content-Type', /json/)
67 | .expect(401, done)
68 | ;
69 | });
70 | var dataViewData = {
71 | owner: 'tester',
72 | name: 'test-api-create',
73 | title: 'My Test DataView',
74 | tmconfig: {
75 | // note a string false - this is what we will get via API (POSTs stringify all values!)
76 | dayfirst: 'false',
77 | startfrom: 'first'
78 | }
79 | };
80 | it('DataView Create and Update OK', function(done) {
81 | request(app)
82 | .post('/api/dataview/')
83 | .send(dataViewData)
84 | .expect('Content-Type', /json/)
85 | .expect(200)
86 | .end(function(err, res) {
87 | assert.deepEqual(res.body, {}, 'Error on API create: ' + JSON.stringify(res.body));
88 | var obj = dao.DataView.create({
89 | owner: dataViewData.owner,
90 | name: dataViewData.name
91 | });
92 | obj.fetch(function(err) {
93 | // console.log(obj);
94 | // console.log(err);
95 | assert(!err, 'New DataView exists');
96 | assert.equal(obj.get('title'), dataViewData.title);
97 | assert.equal(obj.get('tmconfig').dayfirst, false);
98 | testUpdate(obj, done);
99 | });
100 | })
101 | ;
102 | });
103 |
104 | function testUpdate(obj, done) {
105 | var newData = {
106 | title: 'my new title'
107 | };
108 | var newobj = _.extend(obj.toJSON(), newData);
109 | request(app)
110 | .post('/api/dataview/tester/test-api-create')
111 | .send(newobj)
112 | .expect('Content-Type', /json/)
113 | .expect(200)
114 | .end(function(err, res) {
115 | assert.deepEqual(res.body, {}, 'Error on API update: ' + JSON.stringify(res.body));
116 | var obj = dao.DataView.create({
117 | owner: dataViewData.owner,
118 | name: dataViewData.name
119 | });
120 | obj.fetch(function(err) {
121 | assert(!err)
122 | assert.equal(obj.get('title'), newData.title);
123 | done();
124 | });
125 | })
126 | ;
127 | }
128 |
129 | after(function(done) {
130 | var obj = dao.DataView.create(dataViewData);
131 | obj.delete(function() {
132 | var dir = path.join(dao.getBackend().root,
133 | path.dirname(obj.offset())
134 | );
135 | fs.rmdirSync(dir);
136 | done();
137 | });
138 | });
139 | // it('Account Create': function(done) {
140 | // test.expect(1);
141 | // client.fetch('POST', '/api/account', {id: 'new-test-user'}, function(res) {
142 | // test.equal(200, res.statusCode);
143 | // test.done();
144 | // });
145 | // }
146 | // , testAccountUpdate: function(test) {
147 | // test.expect(2);
148 | // client.fetch('PUT', '/api/account/' + base.fixturesData.user.id, {id: 'tester', name: 'my-new-name'}, function(res) {
149 | // test.equal(401, res.statusCode);
150 | // test.deepEqual(res.bodyAsObject, {"error":"Access not allowed","status":401});
151 | // test.done();
152 | // });
153 | // }
154 | });
155 |
156 | describe('API Delete', function() {
157 | before(function(done) {
158 | base.resetDb();
159 | done();
160 | });
161 |
162 | it('Deletes OK', function(done) {
163 | request(app)
164 | .del('/api/dataview/tester/napoleon')
165 | .expect('Content-Type', /json/)
166 | .expect(200)
167 | .end(function(err, res) {
168 | var obj = dao.DataView.create({
169 | owner: 'tester',
170 | name: 'napoleon'
171 | });
172 | obj.fetch(function(err) {
173 | assert(!err);
174 | assert.equal(obj.get('state'), 'deleted');
175 | done();
176 | });
177 | });
178 | });
179 |
180 | // TODO: ...
181 | it('Delete not allowed', function() {
182 | });
183 | });
184 |
185 |
186 | describe('Site', function() {
187 | before(function(done) {
188 | base.resetDb();
189 | done();
190 | });
191 | after(function(done) {
192 | base.resetDb();
193 | done();
194 | });
195 |
196 | it('DataView Edit OK', function(done) {
197 | request(app)
198 | .get('/tester/napoleon/edit')
199 | .end(function(err, res) {
200 | assert.ok(res.text.indexOf('Edit') != -1);
201 | done();
202 | });
203 | });
204 | it('(Pre)view OK', function(done) {
205 | var url = 'https://docs.google.com/a/okfn.org/spreadsheet/ccc?key=0AqR8dXc6Ji4JdERlNXQ3ekttQk5USmFRaTFMYUNJTkE';
206 | var title = 'Abc is the title';
207 | request(app)
208 | .get('/view?url=' + url + '&title=' + title)
209 | .end(function(err, res) {
210 | assert.ok(res.text.indexOf(title) != -1);
211 | done();
212 | });
213 | });
214 |
215 | var dataViewData = {
216 | name: 'test-api-create',
217 | title: 'My Test DataView',
218 | 'tmconfig[dayfirst]': 'false',
219 | 'tmconfig[startfrom]': 'first'
220 | };
221 | // owner will be set to logged in user
222 | var owner = 'tester'
223 |
224 | it('DataView Create POST OK', function(done) {
225 | request(app)
226 | .post('/create')
227 | .type('form')
228 | .send(dataViewData)
229 | .expect(302)
230 | .end(function(err, res) {
231 | assert(!err, err);
232 | assert.equal(res.header['location'], '/' + owner + '/' + dataViewData.name);
233 | var obj = dao.DataView.create({
234 | owner: owner,
235 | name: dataViewData.name
236 | });
237 | obj.fetch(function(err) {
238 | assert(!err, 'New DataView exists');
239 | assert.equal(obj.get('title'), dataViewData.title);
240 | assert.equal(obj.get('tmconfig').dayfirst, false);
241 | var lic = obj.get('licenses');
242 | assert.equal(lic[0].type, 'cc-by');
243 | done();
244 | });
245 | })
246 | ;
247 | });
248 |
249 | it('DataView Update POST OK', function(done) {
250 | var dataViewData = {
251 | title: 'Updated Data View',
252 | 'tmconfig[viewtype]': 'map',
253 | 'tmconfig[dayfirst]': 'false',
254 | 'tmconfig[startfrom]': 'first'
255 | };
256 | var owner = 'tester';
257 |
258 | request(app)
259 | .post('/tester/napoleon/edit')
260 | .type('form')
261 | .send(dataViewData)
262 | .expect(302)
263 | .end(function(err, res) {
264 | assert(!err, err);
265 | assert.equal(res.header['location'], '/tester/napoleon');
266 | var obj = dao.DataView.create({
267 | owner: owner,
268 | name: 'napoleon'
269 | });
270 | obj.fetch(function(err) {
271 | assert(!err, 'DataView exists');
272 | assert.equal(obj.get('title'), dataViewData.title);
273 | assert.equal(obj.get('tmconfig').dayfirst, false);
274 | assert.equal(obj.get('tmconfig').viewtype, 'map');
275 | // existing data unchanged
276 | var lic = obj.get('licenses');
277 | assert.equal(lic[0].type, 'cc-by-sa');
278 | done();
279 | });
280 | })
281 | ;
282 | });
283 | });
284 |
285 | describe('Site - Anonymous Mode', function() {
286 | before(function(done) {
287 | base.resetDb();
288 | // unset testing mode so that we are not logged in
289 | config.set('test:testing', false);
290 | done();
291 | });
292 | after(function(done) {
293 | // set back to testing
294 | config.set('test:testing', true);
295 | done();
296 | });
297 |
298 | var dataViewData = {
299 | title: 'My Test DataView',
300 | 'tmconfig[dayfirst]': 'false',
301 | 'tmconfig[startfrom]': 'first'
302 | };
303 | // owner will be set to logged in user
304 | var owner = 'anon'
305 | it('Create POST OK', function(done) {
306 | request(app)
307 | .post('/create')
308 | .type('form')
309 | .send(dataViewData)
310 | .expect(302)
311 | .end(function(err, res) {
312 | assert(!err, err);
313 | assert.equal(res.header['location'].indexOf('/' + owner), 0);
314 | var name = res.header['location'].split('/')[2]
315 | var obj = dao.DataView.create({
316 | owner: owner,
317 | name: name
318 | });
319 | obj.fetch(function(err) {
320 | assert(!err, 'New DataView exists');
321 | assert.equal(obj.get('title'), dataViewData.title);
322 | assert.equal(obj.get('tmconfig').dayfirst, false);
323 | var lic = obj.get('licenses');
324 | assert.equal(lic[0].type, 'cc-by');
325 | done();
326 | });
327 | })
328 | ;
329 | });
330 | });
331 |
--------------------------------------------------------------------------------
/lib/dao.js:
--------------------------------------------------------------------------------
1 | var fs = require('fs')
2 | , path = require('path')
3 | , util = require('./util')
4 | , config = require('./config')
5 | , knox = require('knox')
6 | , async = require('async')
7 | ;
8 |
9 | // =================================
10 | // Object oriented helpers
11 |
12 | function clone(object) {
13 | function OneShotConstructor(){}
14 | OneShotConstructor.prototype = object;
15 | return new OneShotConstructor();
16 | }
17 |
18 | function forEachIn(object, action) {
19 | for (var property in object) {
20 | if (object.hasOwnProperty(property))
21 | action(property, object[property]);
22 | }
23 | }
24 |
25 | // =================================
26 | // DAO Helpers
27 |
28 | // QueryResult object
29 | //
30 | // Encapsulate result from ElasticSearch queries and providing helper methods
31 | // (like toJSON)
32 | var QueryResult = function(type, data) {
33 | this.type = getDomainObjectClass(type);
34 | this.data = data;
35 | this.total = this.data.hits.total;
36 | this.results = [];
37 | for (i=0;i 0) {
47 | return this.results[0];
48 | } else {
49 | return null;
50 | }
51 | }
52 |
53 | QueryResult.prototype.toJSON = function() {
54 | var out = {
55 | total: this.total
56 | , results: []
57 | };
58 | for (i=0;i';
20 | $('.embed-modal textarea').val(val);
21 | $('.embed-modal').modal();
22 | });
23 | });
24 |
25 | var TimeMapperView = Backbone.View.extend({
26 | events: {
27 | 'click .controls .js-show-toolbox': '_onShowToolbox',
28 | 'submit .toolbox form': '_onSearch'
29 | },
30 |
31 | initialize: function(options) {
32 | var self = this;
33 | this._setupOnHashChange();
34 |
35 | this.datapackage = options.datapackage;
36 | // fix up for datapackage without right structure
37 | if (!this.datapackage.tmconfig) {
38 | this.datapackage.tmconfig = {};
39 | }
40 | this.timelineState = _.extend({}, this.datapackage.tmconfig.timeline, {
41 | nonUSDates: this.datapackage.tmconfig.dayfirst,
42 | timelineJSOptions: _.extend({}, this.datapackage.tmconfig.timelineJSOptions, {
43 | "hash_bookmark": true
44 | })
45 | });
46 | this._setupTwitter();
47 |
48 | // now load the data
49 | this.model.fetch().done(function() {
50 | self.model.query({size: self.model.recordCount})
51 | .done(function() {
52 | self._dataChanges();
53 | self._setStartPosition();
54 | self._onDataLoaded();
55 | });
56 | });
57 | },
58 |
59 | _setStartPosition: function() {
60 | var startAtSlide = 0;
61 | switch (this.datapackage.tmconfig.startfrom) {
62 | case 'start':
63 | // done
64 | break;
65 | case 'end':
66 | startAtSlide = this.model.recordCount - 1;
67 | break;
68 | case 'today':
69 | var dateToday = new Date();
70 | this.model.records.each(function(rec, i) {
71 | if (rec.get('startParsed') < dateToday) {
72 | startAtSlide = i;
73 | }
74 | });
75 | break;
76 | }
77 | this.timelineState.timelineJSOptions = _.extend(this.timelineState.timelineJSOptions, {
78 | "start_at_slide": startAtSlide
79 | }
80 | );
81 | },
82 |
83 | _onDataLoaded: function() {
84 | $('.js-loading').hide();
85 |
86 | // Note: We *have* to postpone setup until now as otherwise timeline
87 | // might try to navigate to a non-existent marker
88 | if (this.datapackage.tmconfig.viewtype === 'timeline') {
89 | // timeline only
90 | $('body').addClass('viewtype-timeline');
91 | // fix height of timeline to be window height minus navbar and footer
92 | $('.timeline-pane').height($(window).height() - 42 - 41);
93 | this._setupTimeline();
94 | } else if (this.datapackage.tmconfig.viewtype === 'map') {
95 | $('body').addClass('viewtype-map');
96 | this._setupMap();
97 | } else {
98 | $('body').addClass('viewtype-timemap');
99 | this._setupTimeline();
100 | this._setupMap();
101 | }
102 |
103 | // Nasty hack. Timeline ignores hashchange events unless is_moving ==
104 | // True. However, once it's True, it can never become false again. The
105 | // callback associated with the UPDATE event sets it to True, but is
106 | // otherwise a no-op.
107 | $("div.slider").trigger("UPDATE");
108 | },
109 |
110 | _setupTwitter: function(e) {
111 | !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
112 | },
113 |
114 | _dataChanges: function() {
115 | var self = this;
116 | this.model.records.each(function(record) {
117 | // normalize date field names
118 | console.log(record)
119 | if (record.get('startdate') || record.get('start date') && !record.get('start')) {
120 | record.set({
121 | start: record.get('startdate') || record.get('start date'),
122 | end: record.get('enddate') || record.get('end date')
123 | }, {silent: true}
124 | );
125 | }
126 | var startDate = VMM.Date.parse(normalizeDate(record.get("start"), self.datapackage.tmconfig.dayfirst));
127 | var data = {
128 | // VMM.Date.parse is the timelinejs date parser
129 | startParsed: startDate,
130 | title: record.get('title') || record.get('headline'),
131 | description: record.get('description') || record.get('text') || '',
132 | url: record.get('url') || record.get('web page'),
133 | media: record.get('image') || record.get('media'),
134 | mediacaption: record.get('caption') || record.get('media caption') || record.get('image caption'),
135 | mediacredit: record.get('image credit') || record.get('media credit'),
136 | };
137 | if (record.get('size') || record.get('size') === 0) {
138 | data.size = parseFloat(record.get('size'));
139 | }
140 | record.set(data, { silent: true });
141 | });
142 |
143 | var starts = this.model.records.pluck('startParsed')
144 | , minDate = _.min(starts)
145 | , maxDate = _.max(starts)
146 | , dateRange = maxDate - minDate
147 | , sizes = this.model.records.pluck('size')
148 | , maxSize = _.max(sizes)
149 | ;
150 | // set opacity - we compute opacity between 0.1 and 1 based on distance from most recent date
151 | var minOpacity = 0.3
152 | , opacityRange = 1.0 - minOpacity
153 | ;
154 | this.model.records.each(function(rec) {
155 | var temporalRangeLocation = (rec.get('startParsed') - minDate) / dateRange;
156 | rec.set({
157 | temporalRangeLocation: temporalRangeLocation,
158 | opacity: minOpacity + (opacityRange * temporalRangeLocation),
159 | relativeSize: parseFloat(rec.get('size')) / maxSize
160 | });
161 | });
162 |
163 | // Timeline will sort the entries by timestamp, and we need the order to be
164 | // the same for the map which runs off the model
165 | this.model.records.comparator = function (a, b) {
166 | return a.get('startParsed') - b.get('startParsed');
167 | };
168 | this.model.records.sort();
169 | },
170 |
171 | _setupOnHashChange: function() {
172 | var self = this;
173 | // listen for hashchange to update map
174 | $(window).on("hashchange", function () {
175 | var hash = window.location.hash.substring(1);
176 | if (parseInt(hash, 10)) {
177 | var record = self.model.records.at(hash);
178 | if (record && record.marker) {
179 | record.marker.openPopup();
180 | }
181 | }
182 | });
183 | },
184 |
185 | _onShowToolbox: function(e) {
186 | e.preventDefault();
187 | if (this.$el.find('.toolbox').hasClass('hideme')) {
188 | this.$el.find('.toolbox').removeClass('hideme');
189 | } else {
190 | this.$el.find('.toolbox').addClass('hideme');
191 | }
192 | },
193 |
194 | _onSearch: function(e) {
195 | e.preventDefault();
196 | var query = this.$el.find('.text-query input').val();
197 | this.model.query({q: query});
198 | },
199 |
200 | _setupTimeline: function() {
201 | this.timeline = new recline.View.Timeline({
202 | model: this.model,
203 | el: this.$el.find('.timeline'),
204 | state: this.timelineState
205 | });
206 |
207 | // convert the record to a structure suitable for timeline.js
208 | this.timeline.convertRecord = function(record, fields) {
209 | if (record.get('startParsed') == 'Invalid Date') {
210 | if (typeof console !== "undefined" && console.warn) {
211 | console.warn('Failed to extract date from record');
212 | console.warn(record.toJSON());
213 | }
214 | return null;
215 | }
216 | try {
217 | var out = this._convertRecord(record, fields);
218 | } catch (e) {
219 | out = null;
220 | }
221 | if (!out) {
222 | if (typeof console !== "undefined" && console.warn) {
223 | console.warn('Failed to extract timeline entry from record');
224 | console.warn(record.toJSON());
225 | }
226 | return null;
227 | }
228 | if (record.get('media')) {
229 | out.asset = {
230 | media: record.get('media'),
231 | caption: record.get('mediacaption'),
232 | credit: record.get('mediacredit'),
233 | thumbnail: record.get('icon')
234 | };
235 | }
236 | out.headline = record.get('title');
237 | if (record.get('url')) {
238 | out.headline = '%headline '
239 | .replace(/%url/g, record.get('url'))
240 | .replace(/%headline/g, out.headline)
241 | ;
242 | }
243 | out.text = record.get('description');
244 | if (record.get('source') || record.get('sourceurl') || record.get('source url')) {
245 | var s = record.get('source') || record.get('sourceurl') || record.get('source url');
246 | if (record.get('source url')) {
247 | s = '' + s + '';
248 | }
249 | out.text += '
Source: ' + s + '
';
250 | }
251 |
252 | return out;
253 | };
254 | this.timeline.render();
255 | },
256 |
257 | _setupMap: function() {
258 | this.map = new recline.View.Map({
259 | model: this.model
260 | });
261 | this.$el.find('.map').append(this.map.el);
262 |
263 | // customize with icon column
264 | this.map.infobox = function(record) {
265 | if (record.icon !== undefined) {
266 | return ' ' + record.get('title');
267 | }
268 | return record.get('title');
269 | };
270 |
271 | this.map.geoJsonLayerOptions.pointToLayer = function(feature, latlng) {
272 | var record = this.model.records.get(feature.properties.cid);
273 | var recordAttr = record.toJSON();
274 | var maxSize = 400;
275 | var radius = parseInt(Math.sqrt(maxSize * recordAttr.relativeSize));
276 | if (radius) {
277 | var marker = new L.CircleMarker(latlng, {
278 | radius: radius,
279 | fillcolor: '#fe9131',
280 | color: '#fe9131',
281 | opacity: recordAttr.opacity,
282 | fillOpacity: recordAttr.opacity * 0.9
283 | });
284 | } else {
285 | var marker = new L.Marker(latlng, {
286 | opacity: recordAttr.opacity
287 | });
288 | }
289 | var label = recordAttr.title + ' Date: ' + recordAttr.start;
290 | if (recordAttr.size) {
291 | label += ' Size: ' + recordAttr.size;
292 | }
293 | marker.bindLabel(label);
294 |
295 | // customize with icon column
296 | if (recordAttr.icon !== undefined) {
297 | var eventIcon = L.icon({
298 | iconUrl: recordAttr.icon,
299 | iconSize: [100, 20], // size of the icon
300 | iconAnchor: [22, 94], // point of the icon which will correspond to marker's location
301 | shadowAnchor: [4, 62], // the same for the shadow
302 | popupAnchor: [-3, -76] // point from which the popup should open relative to the iconAnchor
303 | });
304 | marker.setIcon(eventIcon);
305 | }
306 |
307 | // this is for cluster case
308 | // this.markers.addLayer(marker);
309 |
310 | // When a marker is clicked, update the fragment id, which will in turn update the timeline
311 | marker.on("click", function (e) {
312 | var i = _.indexOf(record.collection.models, record);
313 | window.location.hash = "#" + i.toString();
314 | });
315 |
316 | // Stored so that we can get from record to marker in hashchange callback
317 | record.marker = marker;
318 |
319 | return marker;
320 | };
321 | this.map.render();
322 | }
323 | });
324 |
325 | // convert dates into a format TimelineJS will handle
326 | // TimelineJS does not document this at all so combo of read the code +
327 | // trial and error
328 | // Summary (AFAICt):
329 | // Preferred: [-]yyyy[,mm,dd,hh,mm,ss]
330 | // Supported: mm/dd/yyyy
331 | var normalizeDate = function(date, dayfirst) {
332 | if (!date) {
333 | return '';
334 | }
335 | var out = $.trim(date);
336 | // HACK: support people who put '2013-08-20 in gdocs (to force gdocs to
337 | // not attempt to parse the date)
338 | if (out.length && out[0] === "'") {
339 | out = out.slice(1);
340 | }
341 | out = out.replace(/(\d)th/g, '$1');
342 | out = out.replace(/(\d)st/g, '$1');
343 | out = $.trim(out);
344 | if (out.match(/\d\d\d\d-\d\d-\d\d(T.*)?/)) {
345 | out = out.replace(/-/g, ',').replace('T', ',').replace(':',',');
346 | }
347 | if (out.match(/\d\d-\d\d-\d\d.*/)) {
348 | out = out.replace(/-/g, '/');
349 | }
350 | if (dayfirst) {
351 | var parts = out.match(/(\d\d)\/(\d\d)\/(\d\d.*)/);
352 | if (parts) {
353 | out = [parts[2], parts[1], parts[3]].join('/');
354 | }
355 | }
356 | return out;
357 | }
358 |
359 | })();
360 |
--------------------------------------------------------------------------------
/locale/en_US/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "": {
4 | "Project-Id-Version": " PACKAGE VERSION\nPOT-Creation-Date: 2014-05-31 17:46+0000\nPO-Revision-Date: 2014-05-30 21:10+0800\nLast-Translator: 陳信屹 \nLanguage-Team: English\nLanguage: en_US\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=2; plural=(n != 1);\n"
5 | },
6 | "It's free and easy to use": [
7 | null,
8 | "It's free and easy to use"
9 | ],
10 | "Get started now": [
11 | null,
12 | "Get started now"
13 | ],
14 | "Watch the 1 minute Tutorial": [
15 | null,
16 | "Watch the 1 minute Tutorial"
17 | ],
18 | "Examples": [
19 | null,
20 | "Examples"
21 | ],
22 | "How It Works": [
23 | null,
24 | "How It Works"
25 | ],
26 | "1. Create a Spreadsheet": [
27 | null,
28 | "1. Create a Spreadsheet"
29 | ],
30 | "Add your dates and places to a Google Spreadsheet.": [
31 | null,
32 | "Add your dates and places to a Google Spreadsheet."
33 | ],
34 | "2. Connect and Customize": [
35 | null,
36 | "2. Connect and Customize"
37 | ],
38 | "Connect your spreadsheet with TimeMapper and customize the results.": [
39 | null,
40 | "Connect your spreadsheet with TimeMapper and customize the results."
41 | ],
42 | "3. Publish, Embed and Share": [
43 | null,
44 | "3. Publish, Embed and Share"
45 | ],
46 | "Publish your TimeMap at your own personal url, then share or embed on your site.": [
47 | null,
48 | "Publish your TimeMap at your own personal url, then share or embed on your site."
49 | ],
50 | "Credits": [
51 | null,
52 | "Credits"
53 | ],
54 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs.": [
55 | null,
56 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs."
57 | ],
58 | "It is possible thanks to a set of awesome open-source components including TimelineJS, ReclineJS, Leaflet, Backbone and Bootstrap. You can find the full open-source source for TimeMapper on GitHub here": [
59 | null,
60 | "It is possible thanks to a set of awesome open-source components including TimelineJS, ReclineJS, Leaflet, Backbone and Bootstrap. You can find the full open-source source for TimeMapper on GitHub here"
61 | ],
62 | "Create your": [
63 | null,
64 | ""
65 | ],
66 | "Let's Get Started": [
67 | null,
68 | "Get started now"
69 | ],
70 | "1. Create your Spreadsheet": [
71 | null,
72 | "1. Create a Spreadsheet"
73 | ],
74 | "if you don't have one already!": [
75 | null,
76 | ""
77 | ],
78 | "Get started by copying . For more details or help with problems check out the FAQ below.": [
79 | null,
80 | ""
81 | ],
82 | "Impatient to try this out but don't have a spreadsheet yet?": [
83 | null,
84 | ""
85 | ],
86 | "Click here to use a pre-prepared example": [
87 | null,
88 | ""
89 | ],
90 | "ALERT: you are not signed in so your timemap will be created 'anonymously'.": [
91 | null,
92 | ""
93 | ],
94 | "3. Let's Publish It!": [
95 | null,
96 | ""
97 | ],
98 | "Publish": [
99 | null,
100 | ""
101 | ],
102 | "FAQ": [
103 | null,
104 | ""
105 | ],
106 | "Can I make a timemap anonymously?": [
107 | null,
108 | ""
109 | ],
110 | "If want to 'own' your timemap you should sign in (or sign-up) now »": [
111 | null,
112 | ""
113 | ],
114 | "(Sign-up takes a few seconds with your twitter account »)": [
115 | null,
116 | ""
117 | ],
118 | "Yes! You do not need an account to create a timemap - they can be created anonymously and will have all the same features and shareability of normal timemaps. However, there are some benefits of creating an account and creating your timemap whilst logged in:": [
119 | null,
120 | ""
121 | ],
122 | "You'll get a nice URL for your timemap at /your-username/a-name-you-choose-for-your-timemap": [
123 | null,
124 | ""
125 | ],
126 | "All of your timemaps will be nicely listed at /your-username": [
127 | null,
128 | ""
129 | ],
130 | "As you'll be identified as the owner you'll be able to re-configure (or delete) your timemap later": [
131 | null,
132 | ""
133 | ],
134 | "If you do want an account, signup is very easy": [
135 | null,
136 | ""
137 | ],
138 | "it takes just 15 seconds, is very secure, and uses your Twitter account": [
139 | null,
140 | ""
141 | ],
142 | "(no need to think up a new username and password!).": [
143 | null,
144 | ""
145 | ],
146 | "Go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. This tutorial walks you through.": [
147 | null,
148 | ""
149 | ],
150 | "What URL do I use to connect my spreadsheet?": [
151 | null,
152 | ""
153 | ],
154 | "Use the URL you get by clicking your spreadsheet's Share button and copying the Link to share box.": [
155 | null,
156 | ""
157 | ],
158 | "Note that although you must also Publish to the web, TimeMapper does not use the URL found in the publication pop-up.": [
159 | null,
160 | ""
161 | ],
162 | "What structure must the spreadsheet have?": [
163 | null,
164 | ""
165 | ],
166 | "TimeMapper recognizes certain columns with specific names. The best overview of these columns is the template, which has rows of instructions and examples.": [
167 | null,
168 | ""
169 | ],
170 | "Not all fields are required. The only required fields are Title and Start fields, and even Start can be omitted if you just want a map. Note that you can add any number of other columns beyond those that TimeMapper uses.": [
171 | null,
172 | ""
173 | ],
174 | "How do I format dates?": [
175 | null,
176 | ""
177 | ],
178 | "The preferred date format is ISO 8601 (YYYY-MM-DD), but TimeMapper recognizes most types of date.": [
179 | null,
180 | ""
181 | ],
182 | "If a date's month and day are ambiguous (e.g. is 08-03-1798 UK notation for 8 March, or is it US notation for 3 August?), by default, the first number will be interpreted as the month. You can change this by clicking the edit button in the top right corner of your TimeMap's display and selecting between US- and non-US-style dates.": [
183 | null,
184 | ""
185 | ],
186 | "What kinds of geodata are supported?": [
187 | null,
188 | ""
189 | ],
190 | "The Location column accepts two types of geodata: latitude-longitude coordinates or GeoJSON objects.": [
191 | null,
192 | ""
193 | ],
194 | "Coordinates must be in the format lat, long (e.g. 37.5, -122). The spreadsheet template includes a formula which automatically looks up coordinates corresponding to human-readable place names in the Place column. This formula is explained in a School of Data blog post.": [
195 | null,
196 | ""
197 | ],
198 | "Advanced users who want to go beyond simple coordinates can use GeoJSON feature objects. For an example, see this blog post on adding GeoJSON country boundaries to spreadsheets.": [
199 | null,
200 | ""
201 | ],
202 | "It's as easy as 1-2-3!": [
203 | null,
204 | ""
205 | ],
206 | "Edit - ": [
207 | null,
208 | ""
209 | ],
210 | "Edit your ": [
211 | null,
212 | ""
213 | ],
214 | "Dashboard": [
215 | null,
216 | ""
217 | ],
218 | "Hi there": [
219 | null,
220 | ""
221 | ],
222 | "Create a new Timeline or TimeMap": [
223 | null,
224 | ""
225 | ],
226 | "Your Existing TimeMaps": [
227 | null,
228 | ""
229 | ],
230 | "view": [
231 | null,
232 | ""
233 | ],
234 | "embed": [
235 | null,
236 | ""
237 | ],
238 | "Embed": [
239 | null,
240 | ""
241 | ],
242 | "Edit": [
243 | null,
244 | ""
245 | ],
246 | "Search data ...": [
247 | null,
248 | ""
249 | ],
250 | "Embed Instructions": [
251 | null,
252 | ""
253 | ],
254 | "Copy and paste the following into your web page": [
255 | null,
256 | ""
257 | ],
258 | "Loading data...": [
259 | null,
260 | ""
261 | ],
262 | "using": [
263 | null,
264 | ""
265 | ],
266 | "License": [
267 | null,
268 | ""
269 | ],
270 | "Source Data": [
271 | null,
272 | ""
273 | ],
274 | "Data Source": [
275 | null,
276 | ""
277 | ],
278 | "Select from Your Google Drive": [
279 | null,
280 | ""
281 | ],
282 | "If nothing happens check you are not blocking popups ...": [
283 | null,
284 | ""
285 | ],
286 | "Important": [
287 | null,
288 | ""
289 | ],
290 | "you must \"publish\" your Google Spreadsheet: go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. See": [
291 | null,
292 | ""
293 | ],
294 | "the FAQ below": [
295 | null,
296 | ""
297 | ],
298 | "for more details": [
299 | null,
300 | ""
301 | ],
302 | "Title": [
303 | null,
304 | ""
305 | ],
306 | "Slug": [
307 | null,
308 | ""
309 | ],
310 | "The url of your new timemap. This must be different from the name for any of your existing timemaps. Choose wisely as this is hard to change!": [
311 | null,
312 | ""
313 | ],
314 | "Type of Data View": [
315 | null,
316 | ""
317 | ],
318 | "Choose the visualization type of your data - TimeMap (Timeline and Map combined), Timeline or Map.": [
319 | null,
320 | ""
321 | ],
322 | "More Options": [
323 | null,
324 | ""
325 | ],
326 | "Ambiguous Date Handling": [
327 | null,
328 | ""
329 | ],
330 | "month first (US style)": [
331 | null,
332 | ""
333 | ],
334 | "day first (non-US style)": [
335 | null,
336 | ""
337 | ],
338 | "How to handle ambiguous dates like \"05/08/2012\" in source data (could be read as 5th August or 8th of May).": [
339 | null,
340 | ""
341 | ],
342 | "If you do not have any dates formatted like this then you can ignore this!": [
343 | null,
344 | ""
345 | ],
346 | "Start from": [
347 | null,
348 | ""
349 | ],
350 | "Where on the timeline should the user start.": [
351 | null,
352 | ""
353 | ],
354 | "TimeMapper - Make Timelines and TimeMaps fast!": [
355 | null,
356 | ""
357 | ],
358 | "from the Open Knowledge Foundation Labs": [
359 | null,
360 | ""
361 | ],
362 | "TimeMapper - Make Timelines and TimeMaps fast! - from the Open Knowledge Foundation Labs": [
363 | null,
364 | ""
365 | ],
366 | "An Open Knowledge Foundation Labs Project": [
367 | null,
368 | ""
369 | ],
370 | "Contact Us": [
371 | null,
372 | ""
373 | ],
374 | "Report an Issue": [
375 | null,
376 | ""
377 | ],
378 | "The TimeMapper is a project of Open Knowledge Foundation Labs": [
379 | null,
380 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs."
381 | ],
382 | "TimeMapper is open-source": [
383 | null,
384 | ""
385 | ],
386 | "Source Code": [
387 | null,
388 | ""
389 | ],
390 | "Copyright": [
391 | null,
392 | ""
393 | ],
394 | "Find out more on anonymous vs logged in": [
395 | null,
396 | ""
397 | ],
398 | "read FAQ below": [
399 | null,
400 | ""
401 | ],
402 | "Title for your View": [
403 | null,
404 | ""
405 | ],
406 | "The slug needs to be 'url-usable' and so must be lowercase containing only alphanumeric characters and '-'": [
407 | null,
408 | ""
409 | ],
410 | "Elegant timelines and maps created in seconds": [
411 | null,
412 | ""
413 | ],
414 | "create": [
415 | null,
416 | ""
417 | ],
418 | "Your Spreadsheet": [
419 | null,
420 | "1. Create a Spreadsheet"
421 | ],
422 | "Update": [
423 | null,
424 | ""
425 | ],
426 | "Or paste the Google Spreadsheet URL directly": [
427 | null,
428 | ""
429 | ]
430 | }
431 | }
432 |
--------------------------------------------------------------------------------
/locale/en_US/messages.js:
--------------------------------------------------------------------------------
1 | ;var json_locale_data = {
2 | "messages": {
3 | "": {
4 | "Project-Id-Version": " PACKAGE VERSION\nPOT-Creation-Date: 2014-05-31 17:46+0000\nPO-Revision-Date: 2014-05-30 21:10+0800\nLast-Translator: 陳信屹 \nLanguage-Team: English\nLanguage: en_US\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\nPlural-Forms: nplurals=2; plural=(n != 1);\n"
5 | },
6 | "It's free and easy to use": [
7 | null,
8 | "It's free and easy to use"
9 | ],
10 | "Get started now": [
11 | null,
12 | "Get started now"
13 | ],
14 | "Watch the 1 minute Tutorial": [
15 | null,
16 | "Watch the 1 minute Tutorial"
17 | ],
18 | "Examples": [
19 | null,
20 | "Examples"
21 | ],
22 | "How It Works": [
23 | null,
24 | "How It Works"
25 | ],
26 | "1. Create a Spreadsheet": [
27 | null,
28 | "1. Create a Spreadsheet"
29 | ],
30 | "Add your dates and places to a Google Spreadsheet.": [
31 | null,
32 | "Add your dates and places to a Google Spreadsheet."
33 | ],
34 | "2. Connect and Customize": [
35 | null,
36 | "2. Connect and Customize"
37 | ],
38 | "Connect your spreadsheet with TimeMapper and customize the results.": [
39 | null,
40 | "Connect your spreadsheet with TimeMapper and customize the results."
41 | ],
42 | "3. Publish, Embed and Share": [
43 | null,
44 | "3. Publish, Embed and Share"
45 | ],
46 | "Publish your TimeMap at your own personal url, then share or embed on your site.": [
47 | null,
48 | "Publish your TimeMap at your own personal url, then share or embed on your site."
49 | ],
50 | "Credits": [
51 | null,
52 | "Credits"
53 | ],
54 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs.": [
55 | null,
56 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs."
57 | ],
58 | "It is possible thanks to a set of awesome open-source components including TimelineJS, ReclineJS, Leaflet, Backbone and Bootstrap. You can find the full open-source source for TimeMapper on GitHub here": [
59 | null,
60 | "It is possible thanks to a set of awesome open-source components including TimelineJS, ReclineJS, Leaflet, Backbone and Bootstrap. You can find the full open-source source for TimeMapper on GitHub here"
61 | ],
62 | "Create your": [
63 | null,
64 | ""
65 | ],
66 | "Let's Get Started": [
67 | null,
68 | "Get started now"
69 | ],
70 | "1. Create your Spreadsheet": [
71 | null,
72 | "1. Create a Spreadsheet"
73 | ],
74 | "if you don't have one already!": [
75 | null,
76 | ""
77 | ],
78 | "Get started by copying . For more details or help with problems check out the FAQ below.": [
79 | null,
80 | ""
81 | ],
82 | "Impatient to try this out but don't have a spreadsheet yet?": [
83 | null,
84 | ""
85 | ],
86 | "Click here to use a pre-prepared example": [
87 | null,
88 | ""
89 | ],
90 | "ALERT: you are not signed in so your timemap will be created 'anonymously'.": [
91 | null,
92 | ""
93 | ],
94 | "3. Let's Publish It!": [
95 | null,
96 | ""
97 | ],
98 | "Publish": [
99 | null,
100 | ""
101 | ],
102 | "FAQ": [
103 | null,
104 | ""
105 | ],
106 | "Can I make a timemap anonymously?": [
107 | null,
108 | ""
109 | ],
110 | "If want to 'own' your timemap you should sign in (or sign-up) now »": [
111 | null,
112 | ""
113 | ],
114 | "(Sign-up takes a few seconds with your twitter account »)": [
115 | null,
116 | ""
117 | ],
118 | "Yes! You do not need an account to create a timemap - they can be created anonymously and will have all the same features and shareability of normal timemaps. However, there are some benefits of creating an account and creating your timemap whilst logged in:": [
119 | null,
120 | ""
121 | ],
122 | "You'll get a nice URL for your timemap at /your-username/a-name-you-choose-for-your-timemap": [
123 | null,
124 | ""
125 | ],
126 | "All of your timemaps will be nicely listed at /your-username": [
127 | null,
128 | ""
129 | ],
130 | "As you'll be identified as the owner you'll be able to re-configure (or delete) your timemap later": [
131 | null,
132 | ""
133 | ],
134 | "If you do want an account, signup is very easy": [
135 | null,
136 | ""
137 | ],
138 | "it takes just 15 seconds, is very secure, and uses your Twitter account": [
139 | null,
140 | ""
141 | ],
142 | "(no need to think up a new username and password!).": [
143 | null,
144 | ""
145 | ],
146 | "Go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. This tutorial walks you through.": [
147 | null,
148 | ""
149 | ],
150 | "What URL do I use to connect my spreadsheet?": [
151 | null,
152 | ""
153 | ],
154 | "Use the URL you get by clicking your spreadsheet's Share button and copying the Link to share box.": [
155 | null,
156 | ""
157 | ],
158 | "Note that although you must also Publish to the web, TimeMapper does not use the URL found in the publication pop-up.": [
159 | null,
160 | ""
161 | ],
162 | "What structure must the spreadsheet have?": [
163 | null,
164 | ""
165 | ],
166 | "TimeMapper recognizes certain columns with specific names. The best overview of these columns is the template, which has rows of instructions and examples.": [
167 | null,
168 | ""
169 | ],
170 | "Not all fields are required. The only required fields are Title and Start fields, and even Start can be omitted if you just want a map. Note that you can add any number of other columns beyond those that TimeMapper uses.": [
171 | null,
172 | ""
173 | ],
174 | "How do I format dates?": [
175 | null,
176 | ""
177 | ],
178 | "The preferred date format is ISO 8601 (YYYY-MM-DD), but TimeMapper recognizes most types of date.": [
179 | null,
180 | ""
181 | ],
182 | "If a date's month and day are ambiguous (e.g. is 08-03-1798 UK notation for 8 March, or is it US notation for 3 August?), by default, the first number will be interpreted as the month. You can change this by clicking the edit button in the top right corner of your TimeMap's display and selecting between US- and non-US-style dates.": [
183 | null,
184 | ""
185 | ],
186 | "What kinds of geodata are supported?": [
187 | null,
188 | ""
189 | ],
190 | "The Location column accepts two types of geodata: latitude-longitude coordinates or GeoJSON objects.": [
191 | null,
192 | ""
193 | ],
194 | "Coordinates must be in the format lat, long (e.g. 37.5, -122). The spreadsheet template includes a formula which automatically looks up coordinates corresponding to human-readable place names in the Place column. This formula is explained in a School of Data blog post.": [
195 | null,
196 | ""
197 | ],
198 | "Advanced users who want to go beyond simple coordinates can use GeoJSON feature objects. For an example, see this blog post on adding GeoJSON country boundaries to spreadsheets.": [
199 | null,
200 | ""
201 | ],
202 | "It's as easy as 1-2-3!": [
203 | null,
204 | ""
205 | ],
206 | "Edit - ": [
207 | null,
208 | ""
209 | ],
210 | "Edit your ": [
211 | null,
212 | ""
213 | ],
214 | "Dashboard": [
215 | null,
216 | ""
217 | ],
218 | "Hi there": [
219 | null,
220 | ""
221 | ],
222 | "Create a new Timeline or TimeMap": [
223 | null,
224 | ""
225 | ],
226 | "Your Existing TimeMaps": [
227 | null,
228 | ""
229 | ],
230 | "view": [
231 | null,
232 | ""
233 | ],
234 | "embed": [
235 | null,
236 | ""
237 | ],
238 | "Embed": [
239 | null,
240 | ""
241 | ],
242 | "Edit": [
243 | null,
244 | ""
245 | ],
246 | "Search data ...": [
247 | null,
248 | ""
249 | ],
250 | "Embed Instructions": [
251 | null,
252 | ""
253 | ],
254 | "Copy and paste the following into your web page": [
255 | null,
256 | ""
257 | ],
258 | "Loading data...": [
259 | null,
260 | ""
261 | ],
262 | "using": [
263 | null,
264 | ""
265 | ],
266 | "License": [
267 | null,
268 | ""
269 | ],
270 | "Source Data": [
271 | null,
272 | ""
273 | ],
274 | "Data Source": [
275 | null,
276 | ""
277 | ],
278 | "Select from Your Google Drive": [
279 | null,
280 | ""
281 | ],
282 | "If nothing happens check you are not blocking popups ...": [
283 | null,
284 | ""
285 | ],
286 | "Important": [
287 | null,
288 | ""
289 | ],
290 | "you must \"publish\" your Google Spreadsheet: go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. See": [
291 | null,
292 | ""
293 | ],
294 | "the FAQ below": [
295 | null,
296 | ""
297 | ],
298 | "for more details": [
299 | null,
300 | ""
301 | ],
302 | "Title": [
303 | null,
304 | ""
305 | ],
306 | "Slug": [
307 | null,
308 | ""
309 | ],
310 | "The url of your new timemap. This must be different from the name for any of your existing timemaps. Choose wisely as this is hard to change!": [
311 | null,
312 | ""
313 | ],
314 | "Type of Data View": [
315 | null,
316 | ""
317 | ],
318 | "Choose the visualization type of your data - TimeMap (Timeline and Map combined), Timeline or Map.": [
319 | null,
320 | ""
321 | ],
322 | "More Options": [
323 | null,
324 | ""
325 | ],
326 | "Ambiguous Date Handling": [
327 | null,
328 | ""
329 | ],
330 | "month first (US style)": [
331 | null,
332 | ""
333 | ],
334 | "day first (non-US style)": [
335 | null,
336 | ""
337 | ],
338 | "How to handle ambiguous dates like \"05/08/2012\" in source data (could be read as 5th August or 8th of May).": [
339 | null,
340 | ""
341 | ],
342 | "If you do not have any dates formatted like this then you can ignore this!": [
343 | null,
344 | ""
345 | ],
346 | "Start from": [
347 | null,
348 | ""
349 | ],
350 | "Where on the timeline should the user start.": [
351 | null,
352 | ""
353 | ],
354 | "TimeMapper - Make Timelines and TimeMaps fast!": [
355 | null,
356 | ""
357 | ],
358 | "from the Open Knowledge Foundation Labs": [
359 | null,
360 | ""
361 | ],
362 | "TimeMapper - Make Timelines and TimeMaps fast! - from the Open Knowledge Foundation Labs": [
363 | null,
364 | ""
365 | ],
366 | "An Open Knowledge Foundation Labs Project": [
367 | null,
368 | ""
369 | ],
370 | "Contact Us": [
371 | null,
372 | ""
373 | ],
374 | "Report an Issue": [
375 | null,
376 | ""
377 | ],
378 | "The TimeMapper is a project of Open Knowledge Foundation Labs": [
379 | null,
380 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs."
381 | ],
382 | "TimeMapper is open-source": [
383 | null,
384 | ""
385 | ],
386 | "Source Code": [
387 | null,
388 | ""
389 | ],
390 | "Copyright": [
391 | null,
392 | ""
393 | ],
394 | "Find out more on anonymous vs logged in": [
395 | null,
396 | ""
397 | ],
398 | "read FAQ below": [
399 | null,
400 | ""
401 | ],
402 | "Title for your View": [
403 | null,
404 | ""
405 | ],
406 | "The slug needs to be 'url-usable' and so must be lowercase containing only alphanumeric characters and '-'": [
407 | null,
408 | ""
409 | ],
410 | "Elegant timelines and maps created in seconds": [
411 | null,
412 | ""
413 | ],
414 | "create": [
415 | null,
416 | ""
417 | ],
418 | "Your Spreadsheet": [
419 | null,
420 | "1. Create a Spreadsheet"
421 | ],
422 | "Update": [
423 | null,
424 | ""
425 | ],
426 | "Or paste the Google Spreadsheet URL directly": [
427 | null,
428 | ""
429 | ]
430 | }
431 | }
432 | ;
433 |
--------------------------------------------------------------------------------
/locale/zh_TW/messages.json:
--------------------------------------------------------------------------------
1 | {
2 | "messages": {
3 | "": {
4 | "Project-Id-Version": " PACKAGE VERSION\nPOT-Creation-Date: 2014-05-31 17:46+0000\nPO-Revision-Date: 2014-05-30 21:10+0800\nLast-Translator: 陳信屹 \nLanguage-Team: Chinese (traditional)\nLanguage: zh_TW\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n"
5 | },
6 | "It's free and easy to use": [
7 | null,
8 | "簡單且容易使用"
9 | ],
10 | "Get started now": [
11 | null,
12 | "現在就開始使用"
13 | ],
14 | "Watch the 1 minute Tutorial": [
15 | null,
16 | "觀看一分鐘教學影片"
17 | ],
18 | "Examples": [
19 | null,
20 | "範例"
21 | ],
22 | "How It Works": [
23 | null,
24 | "如何使用"
25 | ],
26 | "1. Create a Spreadsheet": [
27 | null,
28 | "1. 建立一份列表"
29 | ],
30 | "Add your dates and places to a Google Spreadsheet.": [
31 | null,
32 | "在Google試算表加上時間跟地址。"
33 | ],
34 | "2. Connect and Customize": [
35 | null,
36 | "2. 綁定跟客製化"
37 | ],
38 | "Connect your spreadsheet with TimeMapper and customize the results.": [
39 | null,
40 | "將TimeMapper跟你的Google試算表綁在一起並且客製化結果。"
41 | ],
42 | "3. Publish, Embed and Share": [
43 | null,
44 | "3. 公佈,嵌入,以及分享"
45 | ],
46 | "Publish your TimeMap at your own personal url, then share or embed on your site.": [
47 | null,
48 | "用你的專屬網址公佈你的TimeMap,然後分享或嵌入在你的網站上。"
49 | ],
50 | "Credits": [
51 | null,
52 | "鳴謝"
53 | ],
54 | "TimeMapper is an open-source project of Open Knowledge Foundation Labs.": [
55 | null,
56 | "TimeMapper是Open Knowledge Foundation Labs的開源碼專案。"
57 | ],
58 | "It is possible thanks to a set of awesome open-source components including TimelineJS, ReclineJS, Leaflet, Backbone and Bootstrap. You can find the full open-source source for TimeMapper on GitHub here": [
59 | null,
60 | ""
61 | ],
62 | "Create your": [
63 | null,
64 | "建立你的"
65 | ],
66 | "Let's Get Started": [
67 | null,
68 | "現在就開始使用"
69 | ],
70 | "1. Create your Spreadsheet": [
71 | null,
72 | "1. 建立一份試算表"
73 | ],
74 | "if you don't have one already!": [
75 | null,
76 | "如果你還沒有!"
77 | ],
78 | "Get started by copying . For more details or help with problems check out the FAQ below.": [
79 | null,
80 | ""
81 | ],
82 | "Impatient to try this out but don't have a spreadsheet yet?": [
83 | null,
84 | "受不了想立刻試用但還有沒試算表?"
85 | ],
86 | "Click here to use a pre-prepared example": [
87 | null,
88 | "點擊這裡使用我們事先準備的範例"
89 | ],
90 | "ALERT: you are not signed in so your timemap will be created 'anonymously'.": [
91 | null,
92 | "警告:你尚未登入,所以你的timemap會以匿名方式建立。"
93 | ],
94 | "3. Let's Publish It!": [
95 | null,
96 | "3. 發佈它"
97 | ],
98 | "Publish": [
99 | null,
100 | "發佈"
101 | ],
102 | "FAQ": [
103 | null,
104 | "常見問題"
105 | ],
106 | "Can I make a timemap anonymously?": [
107 | null,
108 | "我能匿名使用嗎?"
109 | ],
110 | "If want to 'own' your timemap you should sign in (or sign-up) now »": [
111 | null,
112 | "如果想'擁有'你的timemap,你需要現在登入 (或註冊) »"
113 | ],
114 | "(Sign-up takes a few seconds with your twitter account »)": [
115 | null,
116 | "(使用你的Twiiter帳號 twitter account »只要幾秒鐘)"
117 | ],
118 | "Yes! You do not need an account to create a timemap - they can be created anonymously and will have all the same features and shareability of normal timemaps. However, there are some benefits of creating an account and creating your timemap whilst logged in:": [
119 | null,
120 | "是的!建立timemap不需要帳號 - 它們可以被匿名建立並且跟一般的timemap擁有一樣的功能也能分享。 無論如何,建立一個帳號以及在登入狀態下建立timemap還是有些好處。"
121 | ],
122 | "You'll get a nice URL for your timemap at /your-username/a-name-you-choose-for-your-timemap": [
123 | null,
124 | "你的timemap會在/your-username/a-name-you-choose-for-your-timemap有專屬的URL"
125 | ],
126 | "All of your timemaps will be nicely listed at /your-username": [
127 | null,
128 | "你所有的timemaps會列在/your-username"
129 | ],
130 | "As you'll be identified as the owner you'll be able to re-configure (or delete) your timemap later": [
131 | null,
132 | "當你能被辯認為timemap的擁有者時,之後你將能夠重新設定(或是刪除) 你的timemap"
133 | ],
134 | "If you do want an account, signup is very easy": [
135 | null,
136 | "你果你真的想要一個帳號,註冊非常簡單"
137 | ],
138 | "it takes just 15 seconds, is very secure, and uses your Twitter account": [
139 | null,
140 | "使用你的Twitter帳號,只需要15秒,非常安全"
141 | ],
142 | "(no need to think up a new username and password!).": [
143 | null,
144 | "(不需要想新的使用者名稱跟密碼!)"
145 | ],
146 | "Go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. This tutorial walks you through.": [
147 | null,
148 | "到你的試算表的檔案選單,然後'公佈到網路上',然後點'開始公佈'. 這份教學會帶你走過一遍。"
149 | ],
150 | "What URL do I use to connect my spreadsheet?": [
151 | null,
152 | "哪種網址才是用來綁定我的試算表"
153 | ],
154 | "Use the URL you get by clicking your spreadsheet's Share button and copying the Link to share box.": [
155 | null,
156 | "點擊你的試算表分享按鈕然後複製分享連結"
157 | ],
158 | "Note that although you must also Publish to the web, TimeMapper does not use the URL found in the publication pop-up.": [
159 | null,
160 | "儘管你也必須發佈到網路TimMapper並沒有用當你發佈後出現的彈跳視窗顯示的網址"
161 | ],
162 | "What structure must the spreadsheet have?": [
163 | null,
164 | "試算表要有怎樣的結構?"
165 | ],
166 | "TimeMapper recognizes certain columns with specific names. The best overview of these columns is the template, which has rows of instructions and examples.": [
167 | null,
168 | ""
169 | ],
170 | "Not all fields are required. The only required fields are Title and Start fields, and even Start can be omitted if you just want a map. Note that you can add any number of other columns beyond those that TimeMapper uses.": [
171 | null,
172 | "不是所有的欄位都需要。必備的欄位是Title還有Start。如果只是建立地圖,Start可以不需要。 記得在TimeMmapper使用的欄位之外,你可以加任意數量的其他欄位"
173 | ],
174 | "How do I format dates?": [
175 | null,
176 | "日期要怎麼寫?"
177 | ],
178 | "The preferred date format is ISO 8601 (YYYY-MM-DD), but TimeMapper recognizes most types of date.": [
179 | null,
180 | "日期格式是ISO 8601 (YYYY-MM-DD),但TimeMapper能辨認更多日期形式。"
181 | ],
182 | "If a date's month and day are ambiguous (e.g. is 08-03-1798 UK notation for 8 March, or is it US notation for 3 August?), by default, the first number will be interpreted as the month. You can change this by clicking the edit button in the top right corner of your TimeMap's display and selecting between US- and non-US-style dates.": [
183 | null,
184 | "如果日期的月份跟日子不精確 (e.g (e.g. 08-03-1798 是指英國標示法的三月八號還是指得是美國標示法的八月三號。預設上,第一個數字將會被當成月份,要改變它,你可以點擊在你的TimeMap的左上角的edit按鈕並且在美國格式跟非美國格式之間切換。"
185 | ],
186 | "What kinds of geodata are supported?": [
187 | null,
188 | "支援哪幾種 geodata?"
189 | ],
190 | "The Location column accepts two types of geodata: latitude-longitude coordinates or GeoJSON objects.": [
191 | null,
192 | "Location 欄位接受兩種 geodata: 經緯度或是GeoJSON物件"
193 | ],
194 | "Coordinates must be in the format lat, long (e.g. 37.5, -122). The spreadsheet template includes a formula which automatically looks up coordinates corresponding to human-readable place names in the Place column. This formula is explained in a School of Data blog post.": [
195 | null,
196 | "坐標必須是lat, long (e.g. 37.5, -122)的格式。 includes a formula which automatically looks up coordinates corresponding to human-readable place names in the Place column. This formula is explained in a School of Data blog post."
197 | ],
198 | "Advanced users who want to go beyond simple coordinates can use GeoJSON feature objects. For an example, see this blog post on adding GeoJSON country boundaries to spreadsheets.": [
199 | null,
200 | "進階使用者可以使用GeoJSON物件。舉例來說,請見這份Blog文章 - 在試算表上加GeoJSON 國家邊界。"
201 | ],
202 | "It's as easy as 1-2-3!": [
203 | null,
204 | "1-2-3 超簡單"
205 | ],
206 | "Edit - ": [
207 | null,
208 | "修改 - "
209 | ],
210 | "Edit your ": [
211 | null,
212 | "修改你的 "
213 | ],
214 | "Dashboard": [
215 | null,
216 | ""
217 | ],
218 | "Hi there": [
219 | null,
220 | "你好"
221 | ],
222 | "Create a new Timeline or TimeMap": [
223 | null,
224 | "建立新的時間軸或時間地圖"
225 | ],
226 | "Your Existing TimeMaps": [
227 | null,
228 | "你目前的時間地圖"
229 | ],
230 | "view": [
231 | null,
232 | "檢視"
233 | ],
234 | "embed": [
235 | null,
236 | "嵌入"
237 | ],
238 | "Embed": [
239 | null,
240 | "嵌入"
241 | ],
242 | "Edit": [
243 | null,
244 | "修改"
245 | ],
246 | "Search data ...": [
247 | null,
248 | "搜尋資料中..."
249 | ],
250 | "Embed Instructions": [
251 | null,
252 | "嵌入方式"
253 | ],
254 | "Copy and paste the following into your web page": [
255 | null,
256 | "將下面複製貼上到你的網頁"
257 | ],
258 | "Loading data...": [
259 | null,
260 | "讀取資料中..."
261 | ],
262 | "using": [
263 | null,
264 | "使用"
265 | ],
266 | "License": [
267 | null,
268 | "授權條款"
269 | ],
270 | "Source Data": [
271 | null,
272 | "來源資料"
273 | ],
274 | "Data Source": [
275 | null,
276 | "資料來源"
277 | ],
278 | "Select from Your Google Drive": [
279 | null,
280 | "從你的 Google Drive 選擇"
281 | ],
282 | "If nothing happens check you are not blocking popups ...": [
283 | null,
284 | "如果什麼事都沒發生,檢查你是否有擋彈跳視窗 ..."
285 | ],
286 | "Important": [
287 | null,
288 | "重要"
289 | ],
290 | "you must \"publish\" your Google Spreadsheet: go to File Menu in your spreadsheet, then 'Publish to the Web', then click 'Start Publishing'. See": [
291 | null,
292 | "到你的試算表的檔案選單,然後'公佈到網路上',然後點'開始公佈'. 這份教學會帶你走過一遍。"
293 | ],
294 | "the FAQ below": [
295 | null,
296 | "常見問題如下"
297 | ],
298 | "for more details": [
299 | null,
300 | "更多細節"
301 | ],
302 | "Title": [
303 | null,
304 | "標題"
305 | ],
306 | "Slug": [
307 | null,
308 | ""
309 | ],
310 | "The url of your new timemap. This must be different from the name for any of your existing timemaps. Choose wisely as this is hard to change!": [
311 | null,
312 | "你的新timemap網址。 這必須跟你現有的timemap不同。 謹慎思考名字,因為這很難修改!"
313 | ],
314 | "Type of Data View": [
315 | null,
316 | "資料顯示方式"
317 | ],
318 | "Choose the visualization type of your data - TimeMap (Timeline and Map combined), Timeline or Map.": [
319 | null,
320 | "選擇你資料的視覺化模式 - TimeMap (時間軸與地圖整合),時間軸或地圖"
321 | ],
322 | "More Options": [
323 | null,
324 | "更多選項"
325 | ],
326 | "Ambiguous Date Handling": [
327 | null,
328 | "歧義時間處理"
329 | ],
330 | "month first (US style)": [
331 | null,
332 | "月份優先 (美國格式)"
333 | ],
334 | "day first (non-US style)": [
335 | null,
336 | "日子優先 (美國格式)"
337 | ],
338 | "How to handle ambiguous dates like \"05/08/2012\" in source data (could be read as 5th August or 8th of May).": [
339 | null,
340 | "如何處理在來源資料裡像\"05/08/2012\"(可能念做八月五號或是五月八號)的日期。"
341 | ],
342 | "If you do not have any dates formatted like this then you can ignore this!": [
343 | null,
344 | "如果你的日期格式沒一個長得像這樣,你可以忽略這件事"
345 | ],
346 | "Start from": [
347 | null,
348 | "從何時開始"
349 | ],
350 | "Where on the timeline should the user start.": [
351 | null,
352 | "為時間軸的起點"
353 | ],
354 | "TimeMapper - Make Timelines and TimeMaps fast!": [
355 | null,
356 | "建立新的時間軸或時間地圖"
357 | ],
358 | "from the Open Knowledge Foundation Labs": [
359 | null,
360 | "由Open Knowledge Foundation Labs 開發"
361 | ],
362 | "TimeMapper - Make Timelines and TimeMaps fast! - from the Open Knowledge Foundation Labs": [
363 | null,
364 | "TimeMapper - 快速建立時間軸跟時間地圖! - 由Open Knowledge Foundation Labs 開發"
365 | ],
366 | "An Open Knowledge Foundation Labs Project": [
367 | null,
368 | "一個 Open Knowledge Foundation Labs 專案"
369 | ],
370 | "Contact Us": [
371 | null,
372 | "聯絡我們"
373 | ],
374 | "Report an Issue": [
375 | null,
376 | "回報問題"
377 | ],
378 | "The TimeMapper is a project of Open Knowledge Foundation Labs": [
379 | null,
380 | "TimeMapper是Open Knowledge Foundation Labs的開源碼專案。"
381 | ],
382 | "TimeMapper is open-source": [
383 | null,
384 | "TimeMapper是開放源碼"
385 | ],
386 | "Source Code": [
387 | null,
388 | "來源資料"
389 | ],
390 | "Copyright": [
391 | null,
392 | "版權所有"
393 | ],
394 | "Find out more on anonymous vs logged in": [
395 | null,
396 | "了解更多匿名使用跟登入後使用的差別"
397 | ],
398 | "read FAQ below": [
399 | null,
400 | "常見問題如下"
401 | ],
402 | "Title for your View": [
403 | null,
404 | "你的標題"
405 | ],
406 | "The slug needs to be 'url-usable' and so must be lowercase containing only alphanumeric characters and '-'": [
407 | null,
408 | "slug 必須是網址合法字元,所以只能是小寫英文字母跟'-'"
409 | ],
410 | "Elegant timelines and maps created in seconds": [
411 | null,
412 | "幾秒內就建立美觀的時間軸跟地圖"
413 | ],
414 | "create": [
415 | null,
416 | "建立"
417 | ],
418 | "Your Spreadsheet": [
419 | null,
420 | "1. 建立一份試算表"
421 | ],
422 | "Update": [
423 | null,
424 | "更新"
425 | ],
426 | "Or paste the Google Spreadsheet URL directly": [
427 | null,
428 | "或者直接貼上Google試算表的網址"
429 | ]
430 | }
431 | }
432 |
--------------------------------------------------------------------------------