]> gitweb.michael.orlitzky.com - charm-bypass.git/blob - index.html.in
index.html.in: boldface and improved margins for <legend>
[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="Get 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 <h3>Zone</h3>
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="Get 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 <h3>Origin</h3>
313
314 <div>
315 <input required type="radio" id="origin1" name="origin" value="BAL" />
316 <label for="origin1">Baltimore/Penn (Penn Line)</label>
317 </div>
318 <div>
319 <input required type="radio" id="origin2" name="origin" value="BCA" />
320 <label for="origin2">Baltimore/Camden (Camden Line)</label>
321 </div>
322 <div>
323 <input required type="radio" id="origin3" name="origin" value="BWE" />
324 <label for="origin3">Bowie State (Penn Line)</label>
325 </div>
326 <div>
327 <input required type="radio" id="origin4" name="origin" value="BWI" />
328 <label for="origin4">BWI Airport (Penn Line)</label>
329 </div>
330 <div>
331 <input required type="radio" id="origin5" name="origin" value="CPK" />
332 <label for="origin5">College Park (Camden Line)</label>
333 </div>
334 <div>
335 <input required type="radio" id="origin6" name="origin" value="SEB" />
336 <label for="origin6">Seabrook (Penn Line)</label>
337 </div>
338 <div>
339 <input required type="radio" id="origin7" name="origin" value="WAS" />
340 <label for="origin7">Washington D.C.</label>
341 </div>
342 <div>
343 <input required type="radio" id="origin8" name="origin" value="WBL" />
344 <label for="origin8">West Baltimore (Penn Line)</label>
345 </div>
346
347 <h3>Destination</h3>
348 <div>
349 <input required type="radio" id="destination1" name="destination" value="BAL" />
350 <label for="destination1">Baltimore/Penn (Penn Line)</label>
351 </div>
352 <div>
353 <input required type="radio" id="destination2" name="destination" value="BCA" />
354 <label for="destination2">Baltimore/Camden (Camden Line)</label>
355 </div>
356 <div>
357 <input required type="radio" id="destination3" name="destination" value="BWE" />
358 <label for="destination3">Bowie State (Penn Line)</label>
359 </div>
360 <div>
361 <input required type="radio" id="destination4" name="destination" value="BWI" />
362 <label for="destination4">BWI Airport (Penn Line)</label>
363 </div>
364 <div>
365 <input required type="radio" id="destination5" name="destination" value="CPK" />
366 <label for="destination5">College Park (Camden Line)</label>
367 </div>
368 <div>
369 <input required type="radio" id="destination6" name="destination" value="SEB" />
370 <label for="destination6">Seabrook (Penn Line)</label>
371 </div>
372 <div>
373 <input required type="radio" id="destination7" name="destination" value="WAS" />
374 <label for="destination7">Washington D.C.</label>
375 </div>
376 <div>
377 <input required type="radio" id="destination8" name="destination" value="WBL" />
378 <label for="destination8">West Baltimore (Penn Line)</label>
379 </div>
380
381 <input type="hidden" name="serviceid" value="R" />
382 <input type="hidden" name="servicename" value="MARC Train" />
383 <input type="submit" name="go" value="Get Ticket" />
384 </fieldset>
385 </form>
386 </div>
387
388 @SVGDATA@
389
390 <script>
391
392 /***********************************************/
393 /* First, center the ticket within the browser */
394 /***********************************************/
395
396 function center_ticket() {
397 /* We're relying on the SVG being the full height of the
398 * viewport already, and on the aspect ratio being
399 * preserved. First, find the center of the ticket. */
400 const r = document.getElementById("ticket").getBoundingClientRect();
401 const c = r.left + (r.width / 2);
402
403 /* That's the center-line of the ticket. We want to move it to
404 * the center-line of the viewport. */
405 const vc = document.documentElement.clientWidth / 2;
406
407 /* This is how much we need to translate the SVG */
408 const hdelta = vc - c;
409
410 /* But before we can set the absolute left-coordinate of the
411 * SVG, we need to know where it is now. Note: without the
412 * "px" this doesn't default to pixels like CSS does. */
413 const svg = document.querySelector("svg");
414 svg.style.left = (svg.getBoundingClientRect().left + hdelta) + "px";
415 }
416
417
418 /******************************/
419 /* Set the service identifier */
420 /******************************/
421 function set_service_id() {
422 const sid = document.getElementById("serviceid");
423
424 /* Get the "serviceid" from the querystring if it's there */
425 const params = new URLSearchParams(document.location.search);
426 if (params.get("serviceid")) {
427 sid.textContent = params.get("serviceid");
428 }
429
430 /* Otherwise, leave it at "F" */
431 }
432
433 /************************/
434 /* Set the service name */
435 /************************/
436 function set_service_name() {
437 const sid = document.getElementById("servicename");
438
439 /* Get the "servicename" from the querystring if it's there */
440 const params = new URLSearchParams(document.location.search);
441 if (params.get("servicename")) {
442 sid.textContent = params.get("servicename");
443 }
444
445 /* Otherwise, leave it at "BaltimoreLink" */
446 }
447
448
449 /************************/
450 /* Set the service zone */
451 /************************/
452 function set_service_zone(event, zone) {
453 /* We can take the zone as a parameter too; this allows us to
454 * use this function for the (computed) MARC Train zone and
455 * not just the querystring Commuter Bus zone. The extra
456 * "event" parameter is there for the event listener, which
457 * would otherwise stuff an onload event into the zone
458 * parameter. "Thankfully" javascript lets us call a
459 * two-argument function with one argument and thereby abuse
460 * the event handler for this. */
461 const z = document.getElementById("zone");
462 const params = new URLSearchParams(document.location.search);
463
464 if (zone) {
465 z.textContent = zone;
466 z.style.display = "block"; /* It's hidden by default */
467 }
468 else if (params.get("zone")) {
469 /* Get the "zone" from the querystring if it's there */
470 z.textContent = params.get("zone");
471 z.style.display = "block"; /* It's hidden by default */
472 }
473
474 /* Otherwise, leave it blank (and hidden) */
475 }
476
477 /***********************************************************/
478 /* Resize the ticket background based on the service name */
479 /***********************************************************/
480
481 function resize_ticket() {
482 /* Get the "servicename" from the querystring if it's there */
483 const params = new URLSearchParams(document.location.search);
484 const tbg = document.getElementById("ticketbg");
485 const t = document.getElementById("ticket");
486 const sn = document.getElementById("servicename");
487
488 if (params.get("servicename") == "Commuter Bus") {
489 /* The top of the background is initially at y=246.859, and
490 * we scale it by a factor of 1.12 to y=276.482 for a change
491 * of 29.623. So after we scale it, we translate it upwards
492 * by that amount to put it back where it started. */
493 tbg.setAttribute("transform", "translate(0 -29.623) scale(1 1.12)");
494
495 /* Now translate the entire ticket up by the magic amount, 1/5
496 * of the size change we made to the background. This ratio
497 * was found by measuring pixels in side-by-side screenshots
498 * of BaltimoreLink and Commuter Bus tickets. */
499 t.setAttribute("transform", "translate(0 -9.33)");
500
501 /* More magic numbers discovered by comparing the two
502 * tickets overlayed in inkscape */
503 sn.setAttribute("transform", "translate(0 64.28)");
504 }
505 else if (params.get("servicename") == "MARC Train") {
506 /* insane tricks are explained above */
507 tbg.setAttribute("transform",
508 "translate(0 -72.378) scale(1 1.2932)");
509 t.setAttribute("transform", "translate(0 -67.17)");
510 sn.setAttribute("transform", "translate(0 131.0)");
511 }
512
513 /* Otherwise, leave it alone. The SVG was designed with the
514 * BaltimoreLink ticket in mind */
515 }
516
517 /****************************************/
518 /* Set and reposition the security code */
519 /****************************************/
520
521 function set_code() {
522 const ct = document.getElementById("codetext");
523
524 /* Get the "code" from the querystring if it's there */
525 const params = new URLSearchParams(document.location.search);
526 if (params.get("code")) {
527 ct.textContent = params.get("code").toUpperCase();
528 }
529 else {
530 /* Otherwise, use a random code */
531 const bucket = ["0","1","2","3","4","5","6","7","8","9",
532 "A","B","C","D","E","F","G","H","I","J",
533 "K","L","M","N","O","P","Q","R","S","T",
534 "U","V","W","X","Y","Z"];
535
536 /* Two random ints between 0 and 35 */
537 const i1 = Math.floor(Math.random() * 36);
538 const i2 = Math.floor(Math.random() * 36);
539 const d1 = bucket[i1];
540 const d2 = bucket[i2];
541 ct.textContent = d1 + d2;
542 }
543 }
544
545
546 function center_code() {
547 /* Center the security code inside its red box */
548 const ct = document.getElementById("codetext");
549 const bg = document.getElementById("codebg");
550
551 /* First, find the center of the red box */
552 const r1 = bg.getBoundingClientRect();
553 const c1 = r1.left + (r1.width / 2);
554
555 /* Now the center of the code text */
556 const r2 = ct.getBoundingClientRect();
557 const c2 = r2.left + (r2.width / 2);
558
559 /* What do we add to c2 to make it equal to c1? */
560 const hdelta = c1 - c2;
561
562 /* We've measured everything so far in "client rect"
563 * coordinates, because that's the only available measurement
564 * we have for the width of the <text> element after futzing
565 * with its contents. But when we reposition that <text>
566 * element, it will be by adjusting its "x" attribute, and
567 * that attribute uses a different coordinate system than the
568 * client rect does. Specifically, "x" refers to an offset
569 * within the SVG's coordinate system, and the client rect
570 * coordinates are pixels on-screen. To convert between the
571 * two, we can take the "width" attribute of the background
572 * element and compare it to the width of the background
573 * element's client rect. Since the size of the background is
574 * fixed, this should give us a multiplier that turns client recr
575 * distances (what we have) into SVG distances (what we want) */
576 const client_to_svg = parseFloat(bg.getAttribute("width"))/r1.width;
577
578 /* Convert hdelta from client rect to SVG coordinates */
579 const svg_hdelta = hdelta * client_to_svg;
580
581 /* Since this <text> element has an "x" attribute it's easier for
582 * us to shift that than it is to mess with the "left" style. */
583 ct.setAttribute("x", parseFloat(ct.getAttribute("x")) + svg_hdelta);
584 }
585
586 /*****************************************/
587 /* Next, set up the ticket date and time */
588 /*****************************************/
589
590 function set_ticket_expiry() {
591 /* There are two parameters, time and date, that we store in one
592 * underlying "date" variable. Default both to an hour and a
593 * half from now. This is what the CharmPass app does for
594 * one-way tickets. */
595 const date = new Date();
596
597 /* BaltimoreLink and MARC Train are valid for an hour and a half */
598 let minutes = 90;
599 const params = new URLSearchParams(document.location.search);
600 if (params.get("servicename") == "Commuter Bus") {
601 /* But commuter bus tickets are only valid for ten minutes */
602 minutes = 10;
603 }
604
605 /* We use the low-level get/setTime to change the number of
606 * milliseconds since the epoch that this date represents
607 * Obviously correct, and avoids all suspicious corner cases
608 * for a few more decades. */
609 date.setTime(date.getTime() + (minutes*60*1000));
610
611 tt = document.getElementById("tickettime");
612 tt.textContent = date.toLocaleTimeString();
613
614 const td = document.getElementById("ticketdate");
615 const dateopts = {
616 day: "2-digit",
617 month: "2-digit",
618 year: "2-digit"
619 };
620 td.textContent = date.toLocaleDateString("en-US", dateopts);
621 }
622
623
624 /*********************************************************/
625 /* Finally, the onclick handler for the night/day switch */
626 /*********************************************************/
627
628 /* We always start in "day" mode */
629 is_day = true;
630
631 function set_day() {
632 sky.style.fill = "#efb02f";
633 }
634
635 function set_night() {
636 sky.style.fill = "#143b66";
637 }
638
639 function swap_colors() {
640 if (is_day) {
641 set_night();
642 is_day = false;
643 }
644 else {
645 set_day();
646 is_day = true;
647 }
648 }
649
650
651 /*******************************************************/
652 /* Compute the MARC "zone" from its origin/destination */
653 /*******************************************************/
654
655 /* Sorted on the first component, then the second */
656 const zone_map = {
657 BAL_BWE: 2,
658 BAL_BWI: 1,
659 BAL_SEB: 3,
660 BAL_WAS: 4,
661 BAL_WBL: 1,
662 BCA_CPK_: 3,
663 BCA_WAS_: 4,
664 BWI_BWE: 1,
665 BWI_WAS: 3,
666 BWI_WBL: 1
667 };
668
669 /* Compute the zone (string) for the given origin/destination pair.
670 * If we don't know it or if you chose in invalid pair (destination
671 * not on the same line as your origin?) then the empty string is
672 * returned. */
673 function compute_marc_zone(src, dest) {
674 /* Forward direction key for zone_map */
675 const fwd = src + "_" + dest;
676
677 /* Reverse direction key for zone_map. The zone_map only
678 * has them listed in one direction, so we check both
679 * directions here. */
680 const rev = dest + "_" + src;
681
682 /* The default. Obviously wrong for when we don't
683 * have the necessary data. */
684 let zone = -1;
685
686 if (zone_map[fwd]) {
687 zone = zone_map[fwd];
688 }
689 else if (zone_map[rev]) {
690 zone = zone_map[rev];
691 }
692
693 /* Convert the number to a string */
694 switch (zone) {
695 case 1:
696 return "One Zone";
697 case 2:
698 return "Two Zone";
699 case 3:
700 return "Three Zone";
701 case 4:
702 return "Four Zone";
703 default:
704 return "";
705 }
706 }
707
708 function set_marc_zone() {
709 const params = new URLSearchParams(document.location.search);
710 if (params.get("origin") && params.get("destination")) {
711 const src = params.get("origin");
712 const dest = params.get("destination");
713 const zone = compute_marc_zone(src, dest);
714
715 set_service_zone(null, zone);
716 }
717 }
718
719
720 /*****************************************************/
721 /* Set the origin and destination for the MARC Train */
722 /*****************************************************/
723
724 function set_marc_origin_destination() {
725 const params = new URLSearchParams(document.location.search);
726 if (!params.get("origin") || !params.get("destination")) {
727 return;
728 }
729
730 const src = params.get("origin");
731 const dest = params.get("destination");
732
733 /* origindest contains both the origin and destination */
734 const origindest = document.getElementById("origindest");
735
736 const origin = document.getElementById("origin");
737 const destination = document.getElementById("destination");
738
739 origin.textContent = params.get("origin");
740 destination.textContent = params.get("destination");
741
742 origindest.style.display = "block"; /* It's hidden by default */
743 }
744
745 /******************************************/
746 /* Display the ticket (and hide the menu) */
747 /******************************************/
748
749 function go() {
750 /* To create our "window" onto the scene, we're going to slide the
751 * SVG off the left-hand side of the screen, and we don't want
752 * scroll bars to appear. */
753 document.body.style.overflow = "hidden";
754 document.body.style.margin = "0";
755 document.body.style.padding = "0";
756
757 const svg = document.querySelector("svg");
758 const menu = document.getElementById("menu");
759 svg.style.display = "initial";
760 menu.style.display = "none";
761 }
762
763 /*****************************************************/
764 /* Add event handlers for all of the functions above */
765 /*****************************************************/
766
767 const params = new URLSearchParams(document.location.search);
768 if (params.get("go")) {
769 /* First unhide the SVG (swap it with the form) */
770 window.addEventListener("load", go);
771
772 /* Center the ticket once when the page has loaded */
773 window.addEventListener("load", center_ticket);
774
775 /* Re-center the ticket when the window is resized */
776 window.addEventListener("resize", center_ticket);
777
778 /* Set the service identifier when the page has loaded */
779 window.addEventListener("load", set_service_id);
780
781 /* Set the service name when the page has loaded */
782 window.addEventListener("load", set_service_name);
783
784 /* Set the service zone when the page has loaded */
785 window.addEventListener("load", set_service_zone);
786
787 /* Resize the ticket background if necessary */
788 window.addEventListener("load", resize_ticket);
789
790 /* Set the security code text when the page has loaded */
791 window.addEventListener("load", set_code);
792
793 /* Center the security code text when the page has loaded; in
794 * particular, after we set the code. */
795 window.addEventListener("load", center_code);
796
797 /* Set the ticket expiration date/time upon page load */
798 window.addEventListener("load", set_ticket_expiry);
799
800 /* Set the MARC Train origin and destination, if applicable */
801 window.addEventListener("load", set_marc_origin_destination);
802
803 /* Set the MARC Train zone, if applicable */
804 window.addEventListener("load", set_marc_zone);
805
806 /* Swap colors when the screen is tapped */
807 document.body.addEventListener("click", swap_colors);
808 }
809
810 </script>
811 </body>
812 </html>