Skip to content
This repository

First pass at diary mapping #219

Open
wants to merge 15 commits into from

3 participants

Mikel Maron Tom Hughes Alex Barth
Mikel Maron

Pulls links to OSM objects out of diary body text, and builds up small map displaying features.
Screenshot from 2013-03-22 18:17:14

and others added some commits April 30, 2011
Added a map to the Changeset listing page. Mouseover on the box or th…
…e table row highlights both.
4114190
Added a map to the Changeset listing page. Mouseover on the box or th…
…e table row highlights both.
c060516
Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/views/changeset/_changeset.html.erb
	app/views/changeset/_map.html.erb
	public/javascripts/map.js
826064d
moved up search box a0a2a39
friends changesets db49758
Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/assets/stylesheets/common.css.scss
	app/controllers/changeset_controller.rb
	app/views/layouts/site.html.erb
	config/locales/en.yml
	config/routes.rb
f5dc24a
Mikel Maron Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/controllers/changeset_controller.rb
	config/locales/en.yml
4231e16
Mikel Maron Changesets by nearby users
Diary entries by friends and nearby users
32ca769
Mikel Maron Merge remote-tracking branch 'upstream/master' 959816c
Mikel Maron Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/controllers/changeset_controller.rb
	app/controllers/diary_entry_controller.rb
	app/views/user/view.html.erb
	config/locales/en.yml
	config/routes.rb
8ff3f5f
Tom Hughes Update to rails 3.2.13 4229d86
Tom Hughes Work around JSON encoding issue in rails 3.2.13 db61afc
Tom Hughes Lock iconv to version 0.1 as later ones need ruby 1.9 7cf709a
Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/views/changeset/list.html.erb
e1296c7
scan diary entry body for osm feature refs, and display map
browse.js can now handle a list of features
5be710c
Tom Hughes
Owner

I guess the obvious question here is, what percentage of diary entries actually reference an object in this way? I'm guessing it is very few... After all it's not like it is something that is very easy to do.

Tom Hughes
Owner

There's also a lot of noise in this pull - it would be good to rebase the actual substantive edits on the current master branch.

Alex Barth

@mikelmaron this is kinda cool. I also have user story questions. @tomhughes - could you stage this so we can play with it?

FWIW, I always wish I could drop in a slippy map by writing something like this

![](http://www.openstreetmap.org/?lat=10.47782&lon=-75.48961&zoom=16&layers=M)

"OpenStreetMap flavored markdown"?

Mikel Maron

Ooops, sorry about all those all old commits. I'll work on cleaning this up. mikelmaron@5be710c is the only relevant one.

After that, will look to get this up somewhere so we can all take a look at it running.

Mikel Maron

My intention is to add features that integrate the Diary more with OSM data and maps, make it more attractive to use. Right now there's not a lot of entries which use this construct, but there's not a lot of diary entries anyway, just under 19k. I've recently started using it more, and see good possibilities for constructive map socializing through the diary.

Like the map permalink example you gave @lxbarth, it works by simple cut-n-paste from node and way browse pages.

[](http://www.openstreetmap.org/browse/way/175643411)

The obvious next step is to add a map marker for the lat/lng of the diary entry itself. Scanning the text for permalinks would be simple to add as well.

Tom Hughes
Owner

I've merged this into http://tomh.apis.dev.openstreetmap.org/ now, though it seems to have a few issues:

http://tomh.apis.dev.openstreetmap.org/user/TomH/diary/5

The map is clearly not positioned right, but that may be an interaction with @samanpwbb's work on the diary which is also on that branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 15 unique commits by 5 authors.

Apr 30, 2011
Added a map to the Changeset listing page. Mouseover on the box or th…
…e table row highlights both.
4114190
Added a map to the Changeset listing page. Mouseover on the box or th…
…e table row highlights both.
c060516
May 17, 2011
Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/views/changeset/_changeset.html.erb
	app/views/changeset/_map.html.erb
	public/javascripts/map.js
826064d
May 18, 2011
moved up search box a0a2a39
Dec 14, 2011
friends changesets db49758
Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/assets/stylesheets/common.css.scss
	app/controllers/changeset_controller.rb
	app/views/layouts/site.html.erb
	config/locales/en.yml
	config/routes.rb
f5dc24a
Jan 06, 2012
Mikel Maron Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/controllers/changeset_controller.rb
	config/locales/en.yml
4231e16
Mikel Maron Changesets by nearby users
Diary entries by friends and nearby users
32ca769
Mikel Maron Merge remote-tracking branch 'upstream/master' 959816c
Jun 11, 2012
Mikel Maron Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/controllers/changeset_controller.rb
	app/controllers/diary_entry_controller.rb
	app/views/user/view.html.erb
	config/locales/en.yml
	config/routes.rb
8ff3f5f
Mar 18, 2013
Tom Hughes Update to rails 3.2.13 4229d86
Mar 19, 2013
Tom Hughes Work around JSON encoding issue in rails 3.2.13 db61afc
Mar 20, 2013
Tom Hughes Lock iconv to version 0.1 as later ones need ruby 1.9 7cf709a
Mar 21, 2013
Merge remote-tracking branch 'upstream/master'
Conflicts:
	app/views/changeset/list.html.erb
e1296c7
Mar 22, 2013
scan diary entry body for osm feature refs, and display map
browse.js can now handle a list of features
5be710c
This page is out of date. Refresh to see the latest.
4  Gemfile
@@ -2,7 +2,7 @@
2 2
 source 'http://rubygems.org'
3 3
 
4 4
 # Require rails
5  
-gem 'rails', '3.2.12'
  5
+gem 'rails', '3.2.13'
6 6
 
7 7
 # Require things which have moved to gems in ruby 1.9
8 8
 gem 'bigdecimal', :platforms => :ruby_19
@@ -37,7 +37,7 @@ gem 'ruby-openid', '>= 2.2.0'
37 37
 gem 'redcarpet'
38 38
 
39 39
 # Character conversion support for ruby 1.8
40  
-gem 'iconv', :platforms => :ruby_18
  40
+gem 'iconv', '= 0.1', :platforms => :ruby_18
41 41
 
42 42
 # Load libxml support for XML parsing and generation
43 43
 gem 'libxml-ruby', '>= 2.0.5', :require => 'libxml'
98  Gemfile.lock
@@ -2,12 +2,12 @@ GEM
2 2
   remote: http://rubygems.org/
3 3
   specs:
4 4
     SystemTimer (1.2.3)
5  
-    actionmailer (3.2.12)
6  
-      actionpack (= 3.2.12)
7  
-      mail (~> 2.4.4)
8  
-    actionpack (3.2.12)
9  
-      activemodel (= 3.2.12)
10  
-      activesupport (= 3.2.12)
  5
+    actionmailer (3.2.13)
  6
+      actionpack (= 3.2.13)
  7
+      mail (~> 2.5.3)
  8
+    actionpack (3.2.13)
  9
+      activemodel (= 3.2.13)
  10
+      activesupport (= 3.2.13)
11 11
       builder (~> 3.0.0)
12 12
       erubis (~> 2.7.0)
13 13
       journey (~> 1.0.4)
@@ -15,19 +15,19 @@ GEM
15 15
       rack-cache (~> 1.2)
16 16
       rack-test (~> 0.6.1)
17 17
       sprockets (~> 2.2.1)
18  
-    activemodel (3.2.12)
19  
-      activesupport (= 3.2.12)
  18
+    activemodel (3.2.13)
  19
+      activesupport (= 3.2.13)
20 20
       builder (~> 3.0.0)
21  
-    activerecord (3.2.12)
22  
-      activemodel (= 3.2.12)
23  
-      activesupport (= 3.2.12)
  21
+    activerecord (3.2.13)
  22
+      activemodel (= 3.2.13)
  23
+      activesupport (= 3.2.13)
24 24
       arel (~> 3.0.2)
25 25
       tzinfo (~> 0.3.29)
26  
-    activeresource (3.2.12)
27  
-      activemodel (= 3.2.12)
28  
-      activesupport (= 3.2.12)
29  
-    activesupport (3.2.12)
30  
-      i18n (~> 0.6)
  26
+    activeresource (3.2.13)
  27
+      activemodel (= 3.2.13)
  28
+      activesupport (= 3.2.13)
  29
+    activesupport (3.2.13)
  30
+      i18n (= 0.6.1)
31 31
       multi_json (~> 1.0)
32 32
     arel (3.0.2)
33 33
     bigdecimal (1.1.0)
@@ -42,7 +42,7 @@ GEM
42 42
     coffee-script (2.2.0)
43 43
       coffee-script-source
44 44
       execjs
45  
-    coffee-script-source (1.4.0)
  45
+    coffee-script-source (1.6.2)
46 46
     composite_primary_keys (5.0.12)
47 47
       activerecord (~> 3.2.0, >= 3.2.9)
48 48
     deadlock_retry (1.2.0)
@@ -51,15 +51,15 @@ GEM
51 51
     erubis (2.7.0)
52 52
     execjs (1.4.0)
53 53
       multi_json (~> 1.0)
54  
-    faraday (0.8.5)
  54
+    faraday (0.8.6)
55 55
       multipart-post (~> 1.1)
56 56
     hike (1.2.1)
57 57
     htmlentities (4.3.1)
58 58
     http_accept_language (1.0.2)
59 59
     httpauth (0.2.0)
60  
-    httpclient (2.3.2)
  60
+    httpclient (2.3.3)
61 61
     i18n (0.6.1)
62  
-    i18n-js (3.0.0.rc3)
  62
+    i18n-js (3.0.0.rc5)
63 63
       i18n
64 64
     iconv (0.1)
65 65
     journey (1.0.4)
@@ -67,28 +67,28 @@ GEM
67 67
       railties (>= 3.0, < 5.0)
68 68
       thor (>= 0.14, < 2.0)
69 69
     json (1.7.7)
70  
-    jwt (0.1.5)
71  
-      multi_json (>= 1.0)
  70
+    jwt (0.1.8)
  71
+      multi_json (>= 1.5)
72 72
     libv8 (3.3.10.4)
73  
-    libxml-ruby (2.5.0)
74  
-    mail (2.4.4)
  73
+    libxml-ruby (2.6.0)
  74
+    mail (2.5.3)
75 75
       i18n (>= 0.4.0)
76 76
       mime-types (~> 1.16)
77 77
       treetop (~> 1.4.8)
78 78
     memcached (1.5.0)
79 79
     mime-types (1.21)
80  
-    minitest (4.6.0)
81  
-    multi_json (1.5.1)
  80
+    minitest (4.7.0)
  81
+    multi_json (1.7.1)
82 82
     multi_xml (0.5.3)
83  
-    multipart-post (1.1.5)
84  
-    nokogiri (1.5.6)
  83
+    multipart-post (1.2.0)
  84
+    nokogiri (1.5.8)
85 85
     oauth (0.4.7)
86 86
     oauth-plugin (0.4.1)
87 87
       multi_json
88 88
       oauth (~> 0.4.4)
89 89
       oauth2 (>= 0.5.0)
90 90
       rack
91  
-    oauth2 (0.9.0)
  91
+    oauth2 (0.9.1)
92 92
       faraday (~> 0.8)
93 93
       httpauth (~> 0.1)
94 94
       jwt (~> 0.1.4)
@@ -104,7 +104,7 @@ GEM
104 104
       mime-types
105 105
     pg (0.14.1)
106 106
     polyglot (0.3.3)
107  
-    r2 (0.0.3)
  107
+    r2 (0.1.0)
108 108
     rack (1.4.5)
109 109
     rack-cache (1.2)
110 110
       rack (>= 0.4)
@@ -117,32 +117,32 @@ GEM
117 117
       rack
118 118
     rack-test (0.6.2)
119 119
       rack (>= 1.0)
120  
-    rails (3.2.12)
121  
-      actionmailer (= 3.2.12)
122  
-      actionpack (= 3.2.12)
123  
-      activerecord (= 3.2.12)
124  
-      activeresource (= 3.2.12)
125  
-      activesupport (= 3.2.12)
  120
+    rails (3.2.13)
  121
+      actionmailer (= 3.2.13)
  122
+      actionpack (= 3.2.13)
  123
+      activerecord (= 3.2.13)
  124
+      activeresource (= 3.2.13)
  125
+      activesupport (= 3.2.13)
126 126
       bundler (~> 1.0)
127  
-      railties (= 3.2.12)
128  
-    rails-i18n (0.7.2)
  127
+      railties (= 3.2.13)
  128
+    rails-i18n (0.7.3)
129 129
       i18n (~> 0.5)
130  
-    railties (3.2.12)
131  
-      actionpack (= 3.2.12)
132  
-      activesupport (= 3.2.12)
  130
+    railties (3.2.13)
  131
+      actionpack (= 3.2.13)
  132
+      activesupport (= 3.2.13)
133 133
       rack-ssl (~> 1.3.2)
134 134
       rake (>= 0.8.7)
135 135
       rdoc (~> 3.4)
136 136
       thor (>= 0.14.6, < 2.0)
137 137
     rake (10.0.3)
138  
-    rdoc (3.12.1)
  138
+    rdoc (3.12.2)
139 139
       json (~> 1.4)
140 140
     redcarpet (2.2.2)
141 141
     rinku (1.7.2)
142  
-    ruby-openid (2.2.2)
  142
+    ruby-openid (2.2.3)
143 143
     sanitize (2.0.3)
144 144
       nokogiri (>= 1.4.4, < 1.6)
145  
-    sass (3.2.5)
  145
+    sass (3.2.7)
146 146
     sass-rails (3.2.6)
147 147
       railties (~> 3.2.0)
148 148
       sass (>= 3.1.10)
@@ -155,12 +155,12 @@ GEM
155 155
     therubyracer (0.10.2)
156 156
       libv8 (~> 3.3.10)
157 157
     thor (0.17.0)
158  
-    tilt (1.3.3)
159  
-    timecop (0.5.9.2)
  158
+    tilt (1.3.6)
  159
+    timecop (0.6.1)
160 160
     treetop (1.4.12)
161 161
       polyglot
162 162
       polyglot (>= 0.3.1)
163  
-    tzinfo (0.3.35)
  163
+    tzinfo (0.3.37)
164 164
     uglifier (1.3.0)
165 165
       execjs (>= 0.3.0)
166 166
       multi_json (~> 1.0, >= 1.0.2)
@@ -181,7 +181,7 @@ DEPENDENCIES
181 181
   http_accept_language (>= 1.0.2)
182 182
   httpclient
183 183
   i18n-js (>= 3.0.0.rc2)
184  
-  iconv
  184
+  iconv (= 0.1)
185 185
   jquery-rails
186 186
   libxml-ruby (>= 2.0.5)
187 187
   memcached (>= 1.4.1)
@@ -192,7 +192,7 @@ DEPENDENCIES
192 192
   pg
193 193
   r2
194 194
   rack-cors
195  
-  rails (= 3.2.12)
  195
+  rails (= 3.2.13)
196 196
   rails-i18n (>= 0.6.3)
197 197
   redcarpet
198 198
   rinku (>= 1.2.2)
59  app/assets/javascripts/browse.js
@@ -48,30 +48,39 @@ $(document).ready(function () {
48 48
     $("#object_larger_map").hide();
49 49
     $("#object_edit").hide();
50 50
 
51  
-    var object = {type: params.type, id: params.id};
52  
-
53  
-    if (!params.visible) {
54  
-      object.version = params.version - 1;
  51
+    var objects;
  52
+    if (params.type != "objects") {
  53
+      objects = [ params ];
  54
+    } else {
  55
+      objects = params.objects;
55 56
     }
56 57
 
57  
-    addObjectToMap(object, true, function(extent) {
58  
-      $("#loading").hide();
59  
-      $("#browse_map .geolink").show();
  58
+    var listbounds = new L.LatLngBounds();
  59
+    for (var i = 0; i < objects.length; i++) {
  60
+      var object = {type: objects[i].type, id: objects[i].id};
  61
+  
  62
+      if (!objects[i].visible) {
  63
+        object.version = objects[i].version - 1;
  64
+      }
  65
+
  66
+      addObjectToMap(object, object.length == 1, function(extent) {
  67
+        $("#loading").hide();
  68
+        $("#browse_map .geolink").show();
60 69
 
61  
-      if (extent) {
62  
-        $("a.bbox[data-editor=remote]").click(function () {
63  
-          return remoteEditHandler(extent);
64  
-        });
  70
+        if (extent) {
  71
+          $("a.bbox[data-editor=remote]").click(function () {
  72
+            return remoteEditHandler(extent);
  73
+          });
65 74
 
66  
-        $("a.object[data-editor=remote]").click(function () {
67  
-          return remoteEditHandler(extent, params.type + params.id);
68  
-        });
  75
+          $("a.object[data-editor=remote]").click(function () {
  76
+            return remoteEditHandler(extent, params.type + params.id);
  77
+          });
69 78
 
70  
-        $("#object_larger_map").show();
71  
-        $("#object_edit").show();
  79
+          $("#object_larger_map").show();
  80
+          $("#object_edit").show();
72 81
 
73  
-        var centre = extent.getCenter();
74  
-        updatelinks(centre.lng,
  82
+          var centre = extent.getCenter();
  83
+          updatelinks(centre.lng,
75 84
                     centre.lat,
76 85
                     16, null,
77 86
                     extent.getWestLng(),
@@ -79,12 +88,16 @@ $(document).ready(function () {
79 88
                     extent.getEastLng(),
80 89
                     extent.getNorthLat(),
81 90
                     object);
82  
-      } else {
83  
-        $("#small_map").hide();
84  
-      }
85  
-    });
  91
+          if (object.length != 1) {
  92
+            listbounds.extend(extent);
  93
+            map.fitBounds(listbounds);
  94
+          }
  95
+        } else {
  96
+          $("#small_map").hide();
  97
+        }
  98
+      });
  99
+    }
86 100
   }
87  
-
88 101
   createMenu("area_edit", "area_edit_menu", "right");
89 102
   createMenu("object_edit", "object_edit_menu", "right");
90 103
 });
20  app/controllers/diary_entry_controller.rb
@@ -171,6 +171,7 @@ def view
171 171
     @entry = @this_user.diary_entries.visible.where(:id => params[:id]).first
172 172
     if @entry
173 173
       @title = t 'diary_entry.view.title', :user => params[:display_name], :title => @entry.title
  174
+      @features = scan_text_for_features(@entry.body)
174 175
     else
175 176
       @title = t 'diary_entry.no_such_entry.title', :id => params[:id]
176 177
       render :action => 'no_such_entry', :status => :not_found
@@ -233,4 +234,23 @@ def set_map_location
233 234
       @zoom = 12
234 235
     end
235 236
   end
  237
+
  238
+  def scan_text_for_features(t)
  239
+    object_refs = t.scan(/\[.*?\]\(http:\/\/#{SERVER_URL}\/browse\/(.+?)\/(\d+)\)/).collect { |type, id| {:type => type, :id => id }}
  240
+    a = Array.new
  241
+    object_refs.each do |obj|
  242
+      begin
  243
+        if obj[:type] == "node"
  244
+          d = Node.find(obj[:id])
  245
+        elsif obj[:type] == "way"
  246
+          d = Way.find(obj[:id])
  247
+        end
  248
+        a.push d
  249
+      rescue
  250
+      end
  251
+    end
  252
+    if a.length > 0
  253
+      return a
  254
+    end
  255
+  end
236 256
 end
8  app/views/changeset/_changeset_map_add.html.erb
... ...
@@ -0,0 +1,8 @@
  1
+        var minlon = <%= changeset_map_add.min_lon / GeoRecord::SCALE.to_f %>;
  2
+        var minlat = <%= changeset_map_add.min_lat / GeoRecord::SCALE.to_f %>;
  3
+        var maxlon = <%= changeset_map_add.max_lon / GeoRecord::SCALE.to_f %>;
  4
+        var maxlat = <%= changeset_map_add.max_lat / GeoRecord::SCALE.to_f %>;
  5
+        var bbox = new OpenLayers.Bounds(minlon, minlat, maxlon, maxlat);
  6
+
  7
+        bounds.extend(bbox);
  8
+        box = addBoxToMap(bbox, {name: "changeset-<%= changeset_map_add.id %>"}, true);
4  app/views/diary_entry/_diary_entry.html.erb
@@ -7,6 +7,10 @@
7 7
     <h2><%= link_to h(diary_entry.title), :action => 'view', :display_name => diary_entry.user.display_name, :id => diary_entry.id %></h2>
8 8
   </div>
9 9
 
  10
+  <% if @features %>
  11
+    <%= render :partial => "map", :object => @features %>
  12
+  <% end %>
  13
+
10 14
   <div class="richtext" xml:lang="<%= diary_entry.language_code %>" lang="<%= diary_entry.language_code %>">
11 15
     <%= diary_entry.body.to_html %>
12 16
   </div>
47  app/views/diary_entry/_map.html.erb
... ...
@@ -0,0 +1,47 @@
  1
+<div id="browse_map" class='clearfix content_map'>
  2
+  <% if map.instance_of? Changeset or map.instance_of? Array or (map.instance_of? Node and map.version > 1) or map.visible %>
  3
+
  4
+  <% content_for :head do %>
  5
+    <%= javascript_include_tag "browse" %>
  6
+  <% end %>
  7
+
  8
+  <%
  9
+     if map.instance_of? Changeset
  10
+       bbox = map.bbox.to_unscaled
  11
+       data = {
  12
+         :type   => "changeset",
  13
+         :id     => map.id,
  14
+         :minlon => bbox.min_lon,
  15
+         :minlat => bbox.min_lat,
  16
+         :maxlon => bbox.max_lon,
  17
+         :maxlat => bbox.max_lat
  18
+       }
  19
+     elsif map.instance_of? Array
  20
+       data = {
  21
+         :type => "objects",
  22
+         :objects => Array.new
  23
+       }
  24
+       for object in map do
  25
+         data[:objects].push( { 
  26
+           :type => object.class.name.downcase,
  27
+           :id => object.id,
  28
+           :version => object.version,
  29
+           :visible => object.visible
  30
+           } )
  31
+       end
  32
+     else
  33
+       data = {
  34
+         :type    => map.class.name.downcase,
  35
+         :id      => map.id,
  36
+         :version => map.version,
  37
+         :visible => map.visible
  38
+       }
  39
+     end
  40
+  %>
  41
+  <%= content_tag "div", "", :id => "small_map", :data => data %>
  42
+  <span id="loading"><%= t 'browse.map.loading' %></span>
  43
+
  44
+  <% else %>
  45
+    <%= t 'browse.map.deleted' %>
  46
+  <% end %>
  47
+</div>
2  app/views/diary_entry/view.html.erb
@@ -24,4 +24,4 @@
24 24
 
25 25
 <%= if_not_logged_in(:div) do %>
26 26
   <h3 id="newcomment"><%= raw t("diary_entry.view.login_to_leave_a_comment", :login_link => link_to(t("diary_entry.view.login"), :controller => 'user', :action => 'login', :referer => request.fullpath)) %></h3>
27  
-<% end %>
  27
+<% end %>
23  config/initializers/json_encoding.rb
... ...
@@ -0,0 +1,23 @@
  1
+# Revert a commit (https://github.com/rails/rails/commit/815a9431ab61376a7e8e1bdff21f87bc557992f8)
  2
+# that introduced encoding problems. See https://github.com/rails/rails/issues/9498
  3
+module ActiveSupport
  4
+  module JSON
  5
+    module Encoding
  6
+      def self.escape(string)
  7
+        if string.respond_to?(:force_encoding)
  8
+          string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
  9
+        end
  10
+        json = string.
  11
+          gsub(escape_regex) { |s| ESCAPED_CHARS[s] }.
  12
+          gsub(/([\xC0-\xDF][\x80-\xBF]|
  13
+                 [\xE0-\xEF][\x80-\xBF]{2}|
  14
+                 [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s|
  15
+          s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&')
  16
+        }
  17
+        json = %("#{json}")
  18
+        json.force_encoding(::Encoding::UTF_8) if json.respond_to?(:force_encoding)
  19
+        json
  20
+      end
  21
+    end
  22
+  end
  23
+end
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.