]> gitweb.michael.orlitzky.com - charm-bypass.git/blob - index.html.in
372c3ed3ac681d4cf32741952bbb2442ca60f0ea
[charm-bypass.git] / index.html.in
1 <!doctype html>
2 <html lang="en-US">
3 <head>
4 <meta name="viewport" content="width=device-width, initial-scale=1" />
5 <link rel="icon" href="favicon.svg" />
6
7 <title>
8 CharmBypass: it's transit equity y'all
9 </title>
10
11 <style>
12 input[type=submit] {
13 margin-top: 1em;
14 margin-bottom: 1em;
15 }
16
17 legend {
18 font-weight: bold;
19 margin-top: 1em;
20 margin-bottom: 1em;
21 }
22
23 svg {
24 /* Set the height to 100% of the screen, which we'll keep; and the
25 * initial position to (0,0), which we're going to change
26 * every time the window is resized, because the exact amount
27 * that we have to slide the SVG to the left to get the ticket
28 * into the center will change. */
29 position: fixed;
30 top: 0;
31 left: 0;
32 height: 100%;
33
34 /* Hide everything by default. We only show it once the user has
35 * submitted the menu form */
36 display: none;
37 }
38
39 /* The blinking fade in/out animation for the ticket date and time */
40 @keyframes blink {
41 25% {
42 opacity: 0.5;
43 }
44 50% {
45 opacity: 0;
46 }
47 75% {
48 opacity: 0.5;
49 }
50 }
51
52 #tickettime, #ticketdate {
53 /* 300 two-second blinks is ten minutes */
54 animation: blink 2s linear 300;
55 }
56
57 /* Define, load, and specify the custom font we use for the ticket
58 * date, time, and service name. */
59 @font-face {
60 font-family: "CharmBypass Regular";
61 src:
62 url("data:font/woff2;base64,@CBPREGULAR@") format("woff2")
63 }
64
65 #origindest, #servicename, #tickettime, #ticketdate, #codetext, #zone {
66 font-family: "CharmBypass Regular", sans-serif;
67 }
68
69 @font-face {
70 font-family: "CharmBypass Bold";
71 src:
72 url("data:font/woff2;base64,@CBPBOLD@") format("woff2")
73 }
74
75 #serviceid {
76 font-family: "CharmBypass Bold", sans-serif;
77 }
78
79 /************************/
80 /* Scrolling animations */
81 /************************/
82
83 /* Bus */
84 @keyframes busroll {
85 from {
86 transform: translateX(0%);
87 }
88 to {
89 transform: translateX(-100%);
90 }
91 }
92
93 #bus {
94 animation: busroll 23s linear infinite;
95 }
96
97
98 /* Tram */
99 @keyframes tramroll {
100 from {
101 transform: translateX(0%);
102 }
103 to {
104 transform: translateX(100%);
105 }
106 }
107
108 #tram {
109 animation: tramroll 17s linear infinite;
110 }
111
112
113 /* Tram */
114 @keyframes trainroll {
115 from {
116 transform: translateX(0%);
117 }
118 to {
119 transform: translateX(100%);
120 }
121 }
122
123 #train {
124 animation: trainroll 11s linear infinite;
125 }
126
127
128 /* Clouds */
129 @keyframes cloudsfloat {
130 from {
131 transform: translateX(0%);
132 }
133 to {
134 transform: translateX(-50%);
135 }
136 }
137
138 #clouds {
139 animation: cloudsfloat 40s linear infinite;
140 }
141
142 @keyframes cloudscopyfloat {
143 from {
144 transform: translateX(0%);
145 }
146 to {
147 transform: translateX(-50%);
148 }
149 }
150
151 #cloudscopy {
152 animation: cloudscopyfloat 40s linear infinite;
153 }
154
155
156 /* Trees */
157 @keyframes treespass {
158 from {
159 transform: translateX(0%);
160 }
161 to {
162 transform: translateX(-50%);
163 }
164 }
165
166 #trees {
167 /* The trees move a little faster than the clouds */
168 animation: treespass 30s linear infinite;
169 }
170
171 @keyframes treescopypass {
172 from {
173 transform: translateX(0%);
174 }
175 to {
176 transform: translateX(-50%);
177 }
178 }
179
180 #treescopy {
181 /* The trees move a little faster than the clouds */
182 animation: treescopypass 30s linear infinite;
183 }
184
185
186 /* City skyline */
187 @keyframes cityscroll {
188 from {
189 transform: translateX(0%);
190 }
191 to {
192 transform: translateX(-50%);
193 }
194 }
195
196 #city {
197 /* The city moves faster than the clouds but slower
198 * than the trees */
199 animation: cityscroll 35s linear infinite;
200 }
201
202 @keyframes citycopyscroll {
203 from {
204 transform: translateX(0%);
205 }
206 to {
207 transform: translateX(-50%);
208 }
209 }
210
211 #citycopy {
212 /* The city moves faster than the clouds but slower
213 * than the trees */
214 animation: citycopyscroll 35s linear infinite;
215 }
216 </style>
217 </head>
218
219 <body>
220 <div id="menu">
221 <h1>CharmBypass</h1>
222 <h2>it's transit equity y'all</h2>
223
224 <p>
225 <em>Hint:</em> If you think the driver or fare inspector will
226 actually check it, the daily security code is the same on a
227 $2.00 BaltimoreLink ticket as it is on a $9.00 MARC Train
228 ticket. It's also the same on your friend's phone, or for
229 anyone else in Maryland that day.
230 </p>
231
232 <form>
233 <fieldset>
234 <legend>BaltimoreLink (Bus, Light Rail, Metro)</legend>
235 <div>
236 <label for="code1">
237 Daily security code (optional):
238 </label>
239 <input id="code1"
240 name="code"
241 type="text"
242 size="2"
243 minlength="2"
244 maxlength="2"
245 pattern="[a-zA-Z0-9]*" />
246 </div>
247 <input type="hidden" name="servicename" value="BaltimoreLink" />
248 <input type="submit" name="go" value="Generate Ticket" />
249 </fieldset>
250 </form>
251 <form>
252 <fieldset>
253 <legend>Commuter Bus</legend>
254 <input type="hidden" name="serviceid" value="R" />
255 <input type="hidden" name="servicename" value="Commuter Bus" />
256
257 <div>
258 <label for="code2">
259 Daily security code (optional):
260 </label>
261 <input id="code2"
262 name="code"
263 type="text"
264 size="2"
265 minlength="2"
266 maxlength="2"
267 pattern="[a-zA-Z0-9]*" />
268 </div>
269
270 <p>Zone:</p>
271 <div>
272 <input required type="radio" id="zone1" name="zone" value="Zone 1" />
273 <label for="zone1">Zone 1</label>
274 </div>
275 <div>
276 <input required type="radio" id="zone2" name="zone" value="Zone 2" />
277 <label for="zone2">Zone 2</label>
278 </div>
279 <div>
280 <input required type="radio" id="zone3" name="zone" value="Zone 3" />
281 <label for="zone3">Zone 3</label>
282 </div>
283 <div>
284 <input required type="radio" id="zone4" name="zone" value="Zone 4" />
285 <label for="zone4">Zone 4</label>
286 </div>
287 <div>
288 <input required type="radio" id="zone5" name="zone" value="Zone 5" />
289 <label for="zone5">Zone 5</label>
290 </div>
291
292 <input type="submit" name="go" value="Generate Ticket" />
293 </fieldset>
294 </form>
295 <form>
296 <fieldset>
297 <legend>MARC Train</legend>
298
299 <div>
300 <label for="code3">
301 Daily security code (optional):
302 </label>
303 <input id="code3"
304 name="code"
305 type="text"
306 size="2"
307 minlength="2"
308 maxlength="2"
309 pattern="[a-zA-Z0-9]*" />
310 </div>
311
312 <p>Origin:</p>
313 <div>
314 <input required type="radio" id="origin1" name="origin" value="BAL" />
315 <label for="origin1">Baltimore/Penn (Penn Line)</label>
316 </div>
317 <div>
318 <input required type="radio" id="origin2" name="origin" value="BCA" />
319 <label for="origin2">Baltimore/Camden (Camden Line)</label>
320 </div>
321 <div>
322 <input required type="radio" id="origin3" name="origin" value="BWE" />
323 <label for="origin3">Bowie State (Penn Line)</label>
324 </div>
325 <div>
326 <input required type="radio" id="origin4" name="origin" value="BWI" />
327 <label for="origin4">BWI Airport (Penn Line)</label>
328 </div>
329 <div>
330 <input required type="radio" id="origin5" name="origin" value="CPK" />
331 <label for="origin5">College Park (Camden Line)</label>
332 </div>
333 <div>
334 <input required type="radio" id="origin6" name="origin" value="SEB" />
335 <label for="origin6">Seabrook (Penn Line)</label>
336 </div>
337 <div>
338 <input required type="radio" id="origin7" name="origin" value="WAS" />
339 <label for="origin7">Washington D.C.</label>
340 </div>
341 <div>
342 <input required type="radio" id="origin8" name="origin" value="WBL" />
343 <label for="origin8">West Baltimore (Penn Line)</label>
344 </div>
345
346 <p>Destination:</p>
347 <div>
348 <input required type="radio" id="destination1" name="destination" value="BAL" />
349 <label for="destination1">Baltimore/Penn (Penn Line)</label>
350 </div>
351 <div>
352 <input required type="radio" id="destination2" name="destination" value="BCA" />
353 <label for="destination2">Baltimore/Camden (Camden Line)</label>
354 </div>
355 <div>
356 <input required type="radio" id="destination3" name="destination" value="BWE" />
357 <label for="destination3">Bowie State (Penn Line)</label>
358 </div>
359 <div>
360 <input required type="radio" id="destination4" name="destination" value="BWI" />
361 <label for="destination4">BWI Airport (Penn Line)</label>
362 </div>
363 <div>
364 <input required type="radio" id="destination5" name="destination" value="CPK" />
365 <label for="destination5">College Park (Camden Line)</label>
366 </div>
367 <div>
368 <input required type="radio" id="destination6" name="destination" value="SEB" />
369 <label for="destination6">Seabrook (Penn Line)</label>
370 </div>
371 <div>
372 <input required type="radio" id="destination7" name="destination" value="WAS" />
373 <label for="destination7">Washington D.C.</label>
374 </div>
375 <div>
376 <input required type="radio" id="destination8" name="destination" value="WBL" />
377 <label for="destination8">West Baltimore (Penn Line)</label>
378 </div>
379
380 <input type="hidden" name="serviceid" value="R" />
381 <input type="hidden" name="servicename" value="MARC Train" />
382 <input type="submit" name="go" value="Generate Ticket" />
383 </fieldset>
384 </form>
385 </div>
386
387 @SVGDATA@
388
389 <script>
390
391 /***********************************************/
392 /* First, center the ticket within the browser */
393 /***********************************************/
394
395 function center_ticket() {
396 /* We're relying on the SVG being the full height of the
397 * viewport already, and on the aspect ratio being
398 * preserved. First, find the center of the ticket. */
399 const r = document.getElementById("ticket").getBoundingClientRect();
400 const c = r.left + (r.width / 2);
401
402 /* That's the center-line of the ticket. We want to move it to
403 * the center-line of the viewport. */
404 const vc = document.documentElement.clientWidth / 2;
405
406 /* This is how much we need to translate the SVG */
407 const hdelta = vc - c;
408
409 /* But before we can set the absolute left-coordinate of the
410 * SVG, we need to know where it is now. Note: without the
411 * "px" this doesn't default to pixels like CSS does. */
412 const svg = document.querySelector("svg");
413 svg.style.left = (svg.getBoundingClientRect().left + hdelta) + "px";
414 }
415
416
417 /******************************/
418 /* Set the service identifier */
419 /******************************/
420 function set_service_id() {
421 const sid = document.getElementById("serviceid");
422
423 /* Get the "serviceid" from the querystring if it's there */
424 const params = new URLSearchParams(document.location.search);
425 if (params.get("serviceid")) {
426 sid.textContent = params.get("serviceid");
427 }
428
429 /* Otherwise, leave it at "F" */
430 }
431
432 /************************/
433 /* Set the service name */
434 /************************/
435 function set_service_name() {
436 const sid = document.getElementById("servicename");
437
438 /* Get the "servicename" from the querystring if it's there */
439 const params = new URLSearchParams(document.location.search);
440 if (params.get("servicename")) {
441 sid.textContent = params.get("servicename");
442 }
443
444 /* Otherwise, leave it at "BaltimoreLink" */
445 }
446
447
448 /************************/
449 /* Set the service zone */
450 /************************/
451 function set_service_zone(event, zone) {
452 /* We can take the zone as a parameter too; this allows us to
453 * use this function for the (computed) MARC Train zone and
454 * not just the querystring Commuter Bus zone. The extra
455 * "event" parameter is there for the event listener, which
456 * would otherwise stuff an onload event into the zone
457 * parameter. "Thankfully" javascript lets us call a
458 * two-argument function with one argument and thereby abuse
459 * the event handler for this. */
460 const z = document.getElementById("zone");
461 const params = new URLSearchParams(document.location.search);
462
463 if (zone) {
464 z.textContent = zone;
465 z.style.display = "block"; /* It's hidden by default */
466 }
467 else if (params.get("zone")) {
468 /* Get the "zone" from the querystring if it's there */
469 z.textContent = params.get("zone");
470 z.style.display = "block"; /* It's hidden by default */
471 }
472
473 /* Otherwise, leave it blank (and hidden) */
474 }
475
476 /***********************************************************/
477 /* Resize the ticket background based on the service name */
478 /***********************************************************/
479
480 function resize_ticket() {
481 /* Get the "servicename" from the querystring if it's there */
482 const params = new URLSearchParams(document.location.search);
483 const tbg = document.getElementById("ticketbg");
484 const t = document.getElementById("ticket");
485 const sn = document.getElementById("servicename");
486
487 if (params.get("servicename") == "Commuter Bus") {
488 /* The top of the background is initially at y=246.859, and
489 * we scale it by a factor of 1.12 to y=276.482 for a change
490 * of 29.623. So after we scale it, we translate it upwards
491 * by that amount to put it back where it started. */
492 tbg.setAttribute("transform", "translate(0 -29.623) scale(1 1.12)");
493
494 /* Now translate the entire ticket up by the magic amount, 1/5
495 * of the size change we made to the background. This ratio
496 * was found by measuring pixels in side-by-side screenshots
497 * of BaltimoreLink and Commuter Bus tickets. */
498 t.setAttribute("transform", "translate(0 -9.33)");
499
500 /* More magic numbers discovered by comparing the two
501 * tickets overlayed in inkscape */
502 sn.setAttribute("transform", "translate(0 64.28)");
503 }
504 else if (params.get("servicename") == "MARC Train") {
505 /* insane tricks are explained above */
506 tbg.setAttribute("transform",
507 "translate(0 -72.378) scale(1 1.2932)");
508 t.setAttribute("transform", "translate(0 -67.17)");
509 sn.setAttribute("transform", "translate(0 131.0)");
510 }
511
512 /* Otherwise, leave it alone. The SVG was designed with the
513 * BaltimoreLink ticket in mind */
514 }
515
516 /****************************************/
517 /* Set and reposition the security code */
518 /****************************************/
519
520 function set_code() {
521 const ct = document.getElementById("codetext");
522
523 /* Get the "code" from the querystring if it's there */
524 const params = new URLSearchParams(document.location.search);
525 if (params.get("code")) {
526 ct.textContent = params.get("code").toUpperCase();
527 }
528 else {
529 /* Otherwise, use a random code */
530 const bucket = ["0","1","2","3","4","5","6","7","8","9",
531 "A","B","C","D","E","F","G","H","I","J",
532 "K","L","M","N","O","P","Q","R","S","T",
533 "U","V","W","X","Y","Z"];
534
535 /* Two random ints between 0 and 35 */
536 const i1 = Math.floor(Math.random() * 36);
537 const i2 = Math.floor(Math.random() * 36);
538 const d1 = bucket[i1];
539 const d2 = bucket[i2];
540 ct.textContent = d1 + d2;
541 }
542 }
543
544
545 function center_code() {
546 /* Center the security code inside its red box */
547 const ct = document.getElementById("codetext");
548 const bg = document.getElementById("codebg");
549
550 /* First, find the center of the red box */
551 const r1 = bg.getBoundingClientRect();
552 const c1 = r1.left + (r1.width / 2);
553
554 /* Now the center of the code text */
555 const r2 = ct.getBoundingClientRect();
556 const c2 = r2.left + (r2.width / 2);
557
558 /* What do we add to c2 to make it equal to c1? */
559 const hdelta = c1 - c2;
560
561 /* We've measured everything so far in "client rect"
562 * coordinates, because that's the only available measurement
563 * we have for the width of the <text> element after futzing
564 * with its contents. But when we reposition that <text>
565 * element, it will be by adjusting its "x" attribute, and
566 * that attribute uses a different coordinate system than the
567 * client rect does. Specifically, "x" refers to an offset
568 * within the SVG's coordinate system, and the client rect
569 * coordinates are pixels on-screen. To convert between the
570 * two, we can take the "width" attribute of the background
571 * element and compare it to the width of the background
572 * element's client rect. Since the size of the background is
573 * fixed, this should give us a multiplier that turns client recr
574 * distances (what we have) into SVG distances (what we want) */
575 const client_to_svg = parseFloat(bg.getAttribute("width"))/r1.width;
576
577 /* Convert hdelta from client rect to SVG coordinates */
578 const svg_hdelta = hdelta * client_to_svg;
579
580 /* Since this <text> element has an "x" attribute it's easier for
581 * us to shift that than it is to mess with the "left" style. */
582 ct.setAttribute("x", parseFloat(ct.getAttribute("x")) + svg_hdelta);
583 }
584
585 /*****************************************/
586 /* Next, set up the ticket date and time */
587 /*****************************************/
588
589 function set_ticket_expiry() {
590 /* There are two parameters, time and date, that we store in one
591 * underlying "date" variable. Default both to an hour and a
592 * half from now. This is what the CharmPass app does for
593 * one-way tickets. */
594 const date = new Date();
595
596 /* BaltimoreLink and MARC Train are valid for an hour and a half */
597 let minutes = 90;
598 const params = new URLSearchParams(document.location.search);
599 if (params.get("servicename") == "Commuter Bus") {
600 /* But commuter bus tickets are only valid for ten minutes */
601 minutes = 10;
602 }
603
604 /* We use the low-level get/setTime to change the number of
605 * milliseconds since the epoch that this date represents
606 * Obviously correct, and avoids all suspicious corner cases
607 * for a few more decades. */
608 date.setTime(date.getTime() + (minutes*60*1000));
609
610 tt = document.getElementById("tickettime");
611 tt.textContent = date.toLocaleTimeString();
612
613 const td = document.getElementById("ticketdate");
614 const dateopts = {
615 day: "2-digit",
616 month: "2-digit",
617 year: "2-digit"
618 };
619 td.textContent = date.toLocaleDateString("en-US", dateopts);
620 }
621
622
623 /*********************************************************/
624 /* Finally, the onclick handler for the night/day switch */
625 /*********************************************************/
626
627 /* We always start in "day" mode */
628 is_day = true;
629
630 function set_day() {
631 sky.style.fill = "#efb02f";
632 }
633
634 function set_night() {
635 sky.style.fill = "#143b66";
636 }
637
638 function swap_colors() {
639 if (is_day) {
640 set_night();
641 is_day = false;
642 }
643 else {
644 set_day();
645 is_day = true;
646 }
647 }
648
649
650 /*******************************************************/
651 /* Compute the MARC "zone" from its origin/destination */
652 /*******************************************************/
653
654 /* Sorted on the first component, then the second */
655 const zone_map = {
656 BAL_BWE: 2,
657 BAL_BWI: 1,
658 BAL_SEB: 3,
659 BAL_WAS: 4,
660 BAL_WBL: 1,
661 BCA_CPK_: 3,
662 BCA_WAS_: 4,
663 BWI_BWE: 1,
664 BWI_WAS: 3,
665 BWI_WBL: 1
666 };
667
668 /* Compute the zone (string) for the given origin/destination pair.
669 * If we don't know it or if you chose in invalid pair (destination
670 * not on the same line as your origin?) then the empty string is
671 * returned. */
672 function compute_marc_zone(src, dest) {
673 /* Forward direction key for zone_map */
674 const fwd = src + "_" + dest;
675
676 /* Reverse direction key for zone_map. The zone_map only
677 * has them listed in one direction, so we check both
678 * directions here. */
679 const rev = dest + "_" + src;
680
681 /* The default. Obviously wrong for when we don't
682 * have the necessary data. */
683 let zone = -1;
684
685 if (zone_map[fwd]) {
686 zone = zone_map[fwd];
687 }
688 else if (zone_map[rev]) {
689 zone = zone_map[rev];
690 }
691
692 /* Convert the number to a string */
693 switch (zone) {
694 case 1:
695 return "One Zone";
696 case 2:
697 return "Two Zone";
698 case 3:
699 return "Three Zone";
700 case 4:
701 return "Four Zone";
702 default:
703 return "";
704 }
705 }
706
707 function set_marc_zone() {
708 const params = new URLSearchParams(document.location.search);
709 if (params.get("origin") && params.get("destination")) {
710 const src = params.get("origin");
711 const dest = params.get("destination");
712 const zone = compute_marc_zone(src, dest);
713
714 set_service_zone(null, zone);
715 }
716 }
717
718
719 /*****************************************************/
720 /* Set the origin and destination for the MARC Train */
721 /*****************************************************/
722
723 function set_marc_origin_destination() {
724 const params = new URLSearchParams(document.location.search);
725 if (!params.get("origin") || !params.get("destination")) {
726 return;
727 }
728
729 const src = params.get("origin");
730 const dest = params.get("destination");
731
732 /* origindest contains both the origin and destination */
733 const origindest = document.getElementById("origindest");
734
735 const origin = document.getElementById("origin");
736 const destination = document.getElementById("destination");
737
738 origin.textContent = params.get("origin");
739 destination.textContent = params.get("destination");
740
741 origindest.style.display = "block"; /* It's hidden by default */
742 }
743
744 /******************************************/
745 /* Display the ticket (and hide the menu) */
746 /******************************************/
747
748 function go() {
749 /* To create our "window" onto the scene, we're going to slide the
750 * SVG off the left-hand side of the screen, and we don't want
751 * scroll bars to appear. */
752 document.body.style.overflow = "hidden";
753 document.body.style.margin = "0";
754 document.body.style.padding = "0";
755
756 const svg = document.querySelector("svg");
757 const menu = document.getElementById("menu");
758 svg.style.display = "initial";
759 menu.style.display = "none";
760 }
761
762 /*****************************************************/
763 /* Add event handlers for all of the functions above */
764 /*****************************************************/
765
766 const params = new URLSearchParams(document.location.search);
767 if (params.get("go")) {
768 /* First unhide the SVG (swap it with the form) */
769 window.addEventListener("load", go);
770
771 /* Center the ticket once when the page has loaded */
772 window.addEventListener("load", center_ticket);
773
774 /* Re-center the ticket when the window is resized */
775 window.addEventListener("resize", center_ticket);
776
777 /* Set the service identifier when the page has loaded */
778 window.addEventListener("load", set_service_id);
779
780 /* Set the service name when the page has loaded */
781 window.addEventListener("load", set_service_name);
782
783 /* Set the service zone when the page has loaded */
784 window.addEventListener("load", set_service_zone);
785
786 /* Resize the ticket background if necessary */
787 window.addEventListener("load", resize_ticket);
788
789 /* Set the security code text when the page has loaded */
790 window.addEventListener("load", set_code);
791
792 /* Center the security code text when the page has loaded; in
793 * particular, after we set the code. */
794 window.addEventListener("load", center_code);
795
796 /* Set the ticket expiration date/time upon page load */
797 window.addEventListener("load", set_ticket_expiry);
798
799 /* Set the MARC Train origin and destination, if applicable */
800 window.addEventListener("load", set_marc_origin_destination);
801
802 /* Set the MARC Train zone, if applicable */
803 window.addEventListener("load", set_marc_zone);
804
805 /* Swap colors when the screen is tapped */
806 document.body.addEventListener("click", swap_colors);
807 }
808
809 </script>
810 </body>
811 </html>