Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
66fb2ef
RES-1995: map of groups (on Laravel 10)
edwh Jun 30, 2026
80935da
test: read lang array via Filesystem::getRequire (SonarCloud reliabil…
edwh Jul 1, 2026
cf581b3
RES-1995: fix map-of-groups regressions; add network page map+list
edwh Jul 6, 2026
9535969
lang: drop unused groups.nearest_groups key
edwh Jul 6, 2026
9ccb466
RES-1995: address review feedback on the map
edwh Jul 14, 2026
792931f
RES-1995: minimal=true trims the groups summary to what the map uses
edwh Jul 14, 2026
a686030
RES-1995: split map data from list data - index + on-demand hydration
edwh Jul 15, 2026
2d2805c
Merge remote-tracking branch 'origin/develop' into RES-1995_map_of_gr…
edwh Jul 15, 2026
3979bec
Document the new names-index fields in the OpenAPI annotation
edwh Jul 15, 2026
e040d41
Tests for the hydrate action and index-entry shaping
edwh Jul 15, 2026
6a81944
Names index: cast lat/lng and pivot ids to their documented types
edwh Jul 15, 2026
0b79e1b
Merge remote-tracking branch 'origin/develop' into RES-1995_map_of_gr…
edwh Jul 15, 2026
1dbf435
Map search: import the geocoder stylesheet; translate its strings
edwh Jul 15, 2026
c5689fc
Map feedback: pin anchoring, instant tooltip, and the right "next event"
edwh Jul 21, 2026
ebfeaed
Preload the map's place search with the user's own area
edwh Jul 21, 2026
b5a999b
Only approved events count as a group's next event
edwh Jul 21, 2026
b91d5c0
Let a from-scratch database finish migrating
edwh Jul 21, 2026
f6485a5
Comments should say why the code is as it is, not what changed
edwh Jul 21, 2026
6936692
Open the map on the user's country when they have no town
edwh Jul 21, 2026
5e40c7f
Separate a group's name from its archived badge
edwh Jul 21, 2026
ece9aed
Cut the groups list back to what helps someone choose a group
edwh Jul 21, 2026
8e75430
Make filtering move the map, not just shorten the list
edwh Jul 21, 2026
43e93dd
Cluster the pins on the groups map
edwh Jul 21, 2026
d077bfe
Import supercluster's prebuilt bundle, and stop tracking coverage output
edwh Jul 21, 2026
3640890
Wait for the typing to stop before moving the map
edwh Jul 21, 2026
300b651
Fix the empty map on network pages, and the labels for sharing
edwh Jul 21, 2026
4d8b5a5
Tie component test fixtures to what the store actually produces
edwh Jul 21, 2026
affda70
Fix the stuck list, ambiguous place names, and the search lurch
edwh Jul 21, 2026
3a449da
Point somewhere useful when no groups are in view
edwh Jul 22, 2026
a482f8b
Map feedback: street-level zoom, chunkier sized clusters, split co-lo…
edwh Aug 18, 2026
e42615d
Map styling: brand colours and 2px borders for clusters and pins
edwh Aug 19, 2026
c37f952
Cluster counts in the brand font (Asap)
edwh Aug 20, 2026
cfb91f8
Map: zoom with the mouse wheel; fix warming-page mojibake
edwh Aug 20, 2026
b1928f0
Distance column in the map's group list, nearest-first by default
edwh Aug 20, 2026
69e0c03
Place search finds boroughs: merged place-first Photon queries
edwh Aug 20, 2026
b3e7fdf
Drop the two group button keys the map replaced, and name the map gen…
edwh Sep 2, 2026
d573661
Merge branch 'develop' into RES-1995_map_of_groups_l10
ngm Sep 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ restarters-anonymised.sql
/public/repair-data*.csv
/uploads/*.jpg
.mcp.json

# PHPUnit coverage output, regenerated on every test run.
tests/clover.xml
123 changes: 121 additions & 2 deletions app/Http/Controllers/API/GroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,12 @@
* type="object",
* @OA\Property(property="id", type="integer", example=1),
* @OA\Property(property="name", type="string", example="Group Name"),
* @OA\Property(property="lat", type="number", nullable=true, example=51.5),
* @OA\Property(property="lng", type="number", nullable=true, example=-0.12),
* @OA\Property(property="country", type="string", nullable=true, example="United Kingdom"),
* @OA\Property(property="network_ids", type="array", @OA\Items(type="integer")),
* @OA\Property(property="tag_ids", type="array", @OA\Items(type="integer")),
* @OA\Property(property="archived_at", type="string", format="date-time", nullable=true),
* )
* )
* )
Expand All @@ -269,20 +275,40 @@
'includeArchived' => ['string', 'in:true,false'],
]);

// We only return the group id and name, for speed.
$query = Group::select('idgroups', 'name', 'archived_at');
// We only return a small number of attributes, for speed: this index
// drives the groups map (positions/tooltips) and the client-side
// name/country/network/tag filters, with full rows hydrated on demand
// via /groups/summary?ids=.
$query = Group::select('idgroups', 'name', 'latitude', 'longitude', 'country_code', 'archived_at');

if (!$request->has('includeArchived') || $request->get('includeArchived') == 'false') {
$query = $query->whereNull('archived_at');
}

$groups = $query->get();

// Two cheap lookups instead of per-group relation loads.
$networkIds = \DB::table('group_network')
->whereIn('group_id', $groups->pluck('idgroups'))
->get()
->groupBy('group_id');
$tagIds = \DB::table('grouptags_groups')
->whereIn('group', $groups->pluck('idgroups'))
->get()
->groupBy('group');

$ret = [];

foreach ($groups as $group) {
$ret[] = [
'id' => $group->idgroups,
'name' => $group->name,
'lat' => $group->latitude !== null ? (float) $group->latitude : null,
'lng' => $group->longitude !== null ? (float) $group->longitude : null,
'country' => \App\Helpers\Fixometer::getCountryFromCountryCode($group->country_code),
'network_ids' => $networkIds->has($group->idgroups) ? $networkIds[$group->idgroups]->pluck('network_id')->map(fn ($id) => (int) $id)->all() : [],
// The pivot columns are varchars; the API contract is integers.
'tag_ids' => $tagIds->has($group->idgroups) ? $tagIds[$group->idgroups]->pluck('group_tag')->map(fn ($id) => (int) $id)->all() : [],
'archived_at' => $group->archived_at ? Carbon::parse($group->archived_at)->toIso8601String() : null
];
}
Expand All @@ -292,6 +318,99 @@
];
}

/**
* @OA\Get(
* path="/api/v2/groups/summary",
* operationId="getGroupSummariesv2",
* tags={"Groups"},
* summary="Get list of groups with summary information",
* @OA\Parameter(
* name="archived",
* description="Include archived groups. Default false.",
* required=false,
* in="query",
* @OA\Schema(
* type="boolean"
* )
* ),
* @OA\Parameter(
* name="includeNextEvent",
* description="Include the next event for the group. This makes the call slower. Default false.",
* required=false,
* in="query",
* @OA\Schema(
* type="boolean"
* )
* ),
* @OA\Parameter(
* name="includeCounts",
* description="Include the counts of hosts and restarters. This makes the call slower. Default false.",
* required=false,
* in="query",
* @OA\Schema(
* type="boolean"
* )
* ),
* @OA\Parameter(
* name="ids",
* description="Comma-separated group ids. When present, only these groups are returned (used by the groups list to hydrate the visible rows). Maximum 200 ids.",
* required=false,
* in="query",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Response(
* response=200,
* description="Successful operation",
* @OA\JsonContent(
* @OA\Property(
* property="data",
* title="data",
* description="An array of events",
* type="array",
* @OA\Items(
* @OA\Schema(
* ref="#/components/schemas/GroupSummary"
* ),
* )
* )
* )
* ),
* )
*/

public static function listSummaryv2(Request $request) {
$request->validate([
'archived' => ['string', 'in:true,false'],
'ids' => ['string', 'regex:/^\d+(,\d+)*$/', function ($attribute, $value, $fail) {

Check warning on line 386 in app/Http/Controllers/API/GroupController.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused function parameter "$attribute".

See more on https://sonarcloud.io/project/issues?id=TheRestartProject_restarters.net&issues=AZ9k3wyiTcQFGFZ4NC9D&open=AZ9k3wyiTcQFGFZ4NC9D&pullRequest=887
if (count(explode(',', $value)) > 200) {
$fail('A maximum of 200 ids may be requested at once.');
}
}],
]);

// Eager-load everything the GroupSummary resource touches, otherwise
// each group lazy-loads its relations and the call scales O(N).
$query = Group::with(['networks', 'groupImage.image', 'group_tags']);

if ($request->get('archived', 'false') !== 'true') {
$query = $query->whereNull('archived_at');
}

// The groups list hydrates just its visible rows this way, instead of
// paying to serialise every group on page load.
if ($request->filled('ids')) {
$query = $query->whereIn('idgroups', explode(',', $request->get('ids')));
}

$groups = $query->get();

return [
'data' => \App\Http\Resources\GroupSummaryCollection::make($groups)
];
}

/**
* @OA\Get(
* path="/api/v2/groups/tags",
Expand Down
120 changes: 48 additions & 72 deletions app/Http/Controllers/GroupController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,13 @@
use FixometerFile;
use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Log;
use Notification;
use Spatie\ValidationRules\Rules\Delimited;
use Carbon\Carbon;

class GroupController extends Controller
{
private function indexVariations($tab, $network)

Check failure on line 41 in app/Http/Controllers/GroupController.php

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 24 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=TheRestartProject_restarters.net&issues=AZ8av0J5vAm_7qDFV4eC&open=AZ8av0J5vAm_7qDFV4eC&pullRequest=887
{
//Get current logged in user
$user = Auth::user();
Expand All @@ -60,11 +58,10 @@
} else {
$all_group_tags = collect([]);
}
$networks = Network::all();

// Look for groups we have joined, not just been invited to. We have to explicitly test on deleted_at because
// the normal filtering out of soft deletes won't happen for joins.
$your_groups =array_column(Group::with(['networks'])
$your_groups = array_column(Group::with(['networks'])
->join('users_groups', 'users_groups.group', '=', 'groups.idgroups')
->leftJoin('events', 'events.group', '=', 'groups.idgroups')
->where('users_groups.user', $user->id)
Expand All @@ -75,15 +72,54 @@
->get()
->toArray(), 'idgroups');

// We pass a high limit to the groups nearby; there is a distance limit which will normally kick in first.
$groups_near_you = array_column($user->groupsNearby(1000), 'idgroups');
$nearby_groups = [];
$min_lat = 90;
$max_lat = -90;
$min_lng = 180;
$max_lng = -180;

if ($user->latitude || $user->longitude || $user->country_code) {
// We pass a high limit to the groups nearby; there is a distance limit which will normally kick in first.
$nearby_groups = $user->groupsNearby(1000);

if (empty($nearby_groups) && $user->country_code) {
// groupsNearby() needs coordinates, which a user who has only set their country doesn't have. We
// can still open the map on something better than the whole world by framing the groups in their
// country. If there aren't any then we leave the bounding box alone, which the map reads as "no
// location" and falls back to showing every group.
$nearby_groups = Group::whereNull('archived_at')
->where('approved', true)
->where('country_code', $user->country_code)
->whereNotNull('latitude')
->whereNotNull('longitude')
->get();
}

// Now find the lat/lng bounding box which contains these groups.
foreach ($nearby_groups as $group) {
if ($group->latitude < $min_lat) {
$min_lat = $group->latitude;
}
if ($group->latitude > $max_lat) {
$max_lat = $group->latitude;
}
if ($group->longitude < $min_lng) {
$min_lng = $group->longitude;
}
if ($group->longitude > $max_lng) {
$max_lng = $group->longitude;
}
}
}

return view('group.index', [
'groups' => GroupController::expandGroups($groups, $your_groups, $groups_near_you),
'your_groups' => $your_groups,
'nearby_groups' => [ [ $min_lat, $min_lng ], [ $max_lat, $max_lng ] ],
'your_area' => $user->location,
'tab' => $tab,
'your_lat' => $user->latitude,
'your_lng' => $user->longitude,
'tab' => (!$tab || $tab === 'mine') ? 'mine' : 'other',
'network' => $network,
'networks' => $networks,
'all_group_tags' => $all_group_tags,
]);
}
Expand All @@ -105,7 +141,9 @@

public function network($id)
{
return $this->indexVariations('all', $id);
// Retired: network coordinators now see their groups on the network
// page itself (map + list). Kept as a redirect for old links.
return redirect('/networks/' . $id);
}

public function create(Request $request)
Expand Down Expand Up @@ -481,68 +519,6 @@
}
}

public static function expandGroups($groups, $your_groupids, $nearby_groupids)
{
$ret = [];
$user = Auth::user();

if ($groups) {
foreach ($groups as $group) {
$group_image = $group->groupImage;

$event = $group->nextUpcomingParty;

// We want to return the distance from our own location.
$distance = null;
$grouplat = $group->latitude;
$grouplng = $group->longitude;
$userlat = $user->latitude;
$userlng = $user->longitude;

if ($grouplat !== null && $grouplng !== null && $userlat !== null && $userlng !== null) {
if ($grouplat == $userlat && $grouplng == $userlng) {
$distance = 0;
} else {
$distance = 6371 * acos( cos(deg2rad($userlat)) * cos(deg2rad($grouplat)) * cos(deg2rad($grouplng) -
deg2rad($userlng)) + sin(deg2rad($userlat) ) * sin(deg2rad($grouplat)));
}
}

$ret[] = [
'idgroups' => $group->idgroups,
'name' => $group->name,
'image' => (is_object($group_image) && is_object($group_image->image)) ?
asset('uploads/mid_'.$group_image->image->path) : null,
'location' => [
'location' => rtrim($group->location),
'country' => Fixometer::getCountryFromCountryCode($group->country_code),
'country_code' => $group->country_code,
'distance' => $distance,
],
'next_event' => $event ? $event->event_date_local : null,
'all_restarters_count' => $group->all_restarters_count,
'all_hosts_count' => $group->all_hosts_count,
'all_confirmed_restarters_count' => $group->all_confirmed_restarters_count,
'all_confirmed_hosts_count' => $group->all_confirmed_hosts_count,
'networks' => \Illuminate\Support\Arr::pluck($group->networks, 'id'),
'group_tags' => $group->group_tags->pluck('id'),
'group_tags_full' => $group->group_tags->map(function($tag) {
return [
'id' => $tag->id,
'name' => $tag->tag_name,
'network_id' => $tag->network_id,
];
}),
'following' => in_array($group->idgroups, $your_groupids),
'nearby' => in_array($group->idgroups, $nearby_groupids),
'archived_at' => $group->archived_at ? Carbon::parse($group->archived_at)->toIso8601String() : null
];
}
}

return $ret;
}

public static function stats($id, $format = 'row')
{
$group = Group::where('idgroups', $id)->first();
Expand Down
Loading